--------------------------------------------------------------------------
-- $File: //scripts/astrada/ServerModMain.lua $
-- $Revision$
-- $Date$
-- $Author$
--
-- $Id$
--------------------------------------------------------------------------
-- This file is maintained as part of the SSM AEGIS project. All additions
-- are the sole work of their authors to which all copyright and ownership
-- resides. However, each author grants license for the use of his or her
-- work to be a part of the SSM AEGIS project.
--
--  (C), Daniel 'Rod-Serling' Phelps, 2008-2009
--  (C), Terry 'Charlie' Reinert, 2008
--  Edited/Expanded By:
--  (C), Thomas 'ThMage' Hunter, 2009
--  (C), Richard 'DarkLite' May, 2009
--  (C), Bernd 'TITAN', 2009
--------------------------------------------------------------------------
--  This work is based off of:
--	(C), Ralf 'Hawkeye' Ullrich, 2008
--	ServerModMain.lua, v 1.1.2.5 2008/09/22 16:40:54
--  See crysis.jnana.de for full original versions.
--------------------------------------------------------------------------


-- ===============================================
-- Server Side Modification Main
-- ===============================================

AEGIS.VERSION = "Crysis Wars SSM Aegis 1.4";
XCfgVar = XMergeTable(XCloneTable(XDefaultSetup), XDefaultConfig);
XRuntimeVar = XCloneTable(XRuntimeDefaults);

-- ===============================================
-- Constants
-- ===============================================

X_RANK_INDEX = {
	PVT = 1;
	CPL = 2;
	SGT = 3;
	LT  = 4;
	CPT = 5;
	MAJ = 6;
	COL = 7;
	GEN = 8;
};

X_TEAM_INDEX = {
	none = 0;  -- used in InstantAction
	nk = 1;
	us = 2;
	["0"] = 0;
	["1"] = 1;
	["2"] = 2;
};

X_TEAM_LABEL = {
	[0] = "Spectator";
	[1] = "NK";
	[2] = "US";
};

X_TEAM_COLOR = {
	[0] = "$1";
	[1] = "$7";
	[2] = "$5";
};

X_TOGGLE_ID = {
	toggle = -1;
	set = 1;
	reset = 0;
	clear = 0;
	[1] = 1;
	[0] = 0;
	["1"] = 1;
	["0"] = 0;
	["true"] = 1;
	["false"] = 0;
	[true] = 1;
	[false] = 0;
	yes = 1;
	no = 0;
	on = 1;
	off = 0;
};

X_ONOFF_LABEL = {
	[0] = "off";
	[1] = "on";
	[true] = "on";
	[false] = "off";
}

X_CONSOLE_COLOR = {
	white	= "$1",
	blue	= "$2",
	green	= "$3",
	red	    = "$4",
	cyan	= "$5",
	yellow	= "$6",
	magenta	= "$7",
	orange	= "$8",
	grey	= "$9",
	black	= "$0",
};

X_SUITMODE_LABEL = {
	[0] = "speed";
	[1] = "strength";
	[2] = "cloak";
	[3] = "armor";
};
-- ===============================================
-- Core Variables
-- ===============================================

AEGIS.PlayerInformationByProfile = {}; --Initial Setup of Information

XVar = {
--	timerId = 0;
	lastMapUseTime = 0;
	mapthenactive = false;

	nextSpamMessageTime = 0;
	nextSpamMessageIndex = 1;

	adminMessages = {};
	adminMessageRepeatCount = 0;
	adminSpamMsg = nil;
	adminSpamRepeatCount = 0;

	onUpdateQueue = {};
	selfTerminationRequests = {};  -- array of {player = <>, timeOfRequest = <>}

	chatMessages = {};

	time = _time;
	volatileTrust = {};
	aegisVersion = AEGIS.VERSION; -- make it available for spam messages
	punkbusterDetected = false;
	lastStatusScan = _time;

	vote = {};
};

-- ===============================================
-- begin of code
-- ===============================================

-- ===============================================
-- Load Configuration
-- ===============================================

-- default XSetup callbacks
XSetupDefault = {};
XSetupDefault.unknown = {};
XSetupDefault.unknown.unknown = {};
XSetupDefault.unknown.unknown.OnNewMap = function(mapRules, mapName) end;
XSetupDefault.unknown.unknown.OnTimer = function(mapRules, mapName) end;
XSetupDefault.known = {};
XSetupDefault.known.unknown = {};
XSetupDefault.known.unknown.OnNewMap = function(mapRules, mapName) XSetup.unknown.unknown.OnNewMap(mapRules, mapName); end;
XSetupDefault.known.unknown.OnTimer = function(mapRules, mapName) XSetup.unknown.unknown.OnTimer(mapRules, mapName); end;
XSetupDefault.known.known = {};
XSetupDefault.known.known.OnNewMap = function(mapRules, mapName) _G.XSetup[mapRules].unknown.OnNewMap(mapRules, mapName); end;
XSetupDefault.known.known.OnTimer = function(mapRules, mapName) _G.XSetup[mapRules].unknown.OnTimer(mapRules, mapName); end;

-- prepare the XSetup table hierarchy from map info
function XPrepareSetupTables()
	XConfig = {};
	XChatVar = {};
	XSetup = {};
	XSetup.unknown = {};
	XSetup.unknown.unknown = {};
	XSetup.unknown.unknown.OnNewMap = XSetupDefault.unknown.unknown.OnNewMap;
	XSetup.unknown.unknown.OnTimer = XSetupDefault.unknown.unknown.OnTimer;

	for i, rules in ipairs({"InstantAction", "TeamInstantAction", "PowerStruggle"}) do
		XSetup[rules] = {};
		XSetup[rules].unknown = {};
		XSetup[rules].unknown.OnNewMap = XSetupDefault.known.unknown.OnNewMap;
		XSetup[rules].unknown.OnTimer = XSetupDefault.known.unknown.OnTimer;
		if (XVar.mapPaths) then
			for j, mappath in pairs(XVar.mapPaths) do
				local map = string.gsub(mappath, "%W", "_");
				XSetup[rules][map] = {};
				XSetup[rules][map].OnNewMap = XSetupDefault.known.known.OnNewMap;
				XSetup[rules][map].OnTimer = XSetupDefault.known.known.OnTimer;
			end
		end

	end
end

function XMergeHierarchicalSetup()
	local mapRules = XVar.currentMapInfo.mapRules;
	local mapName = XVar.currentMapInfo.mapName;
	for varName, defaultValue in pairs(XDefaultSetup) do
		local tmp = nil;
		tmp = XMergeTable(tmp, defaultValue);
		tmp = XMergeTable(tmp, XConfig[varName]);
		tmp = XMergeTable(tmp,
			( ( (XSetup or {}).unknown or {}).unknown or {})[varName]);
		tmp = XMergeTable(tmp,
			( ( (XSetup or {})[mapRules] or {}).unknown or {})[varName]);
		tmp = XMergeTable(tmp,
			( ( (XSetup or {})[mapRules] or {})[mapName] or {})[varName]);
		if (tmp == nil) then
			-- fall back to default if merged as nil
			tmp = XMergeTable(tmp, defaultValue);
		end
		XCfgVar[varName] = tmp;
	end
	for varName, defaultValue in pairs(XDefaultConfig) do
		local tmp = nil;
		tmp = XMergeTable(tmp, defaultValue);
		tmp = XMergeTable(tmp, XConfig[varName]);
		if (tmp == nil) then
			-- fall back to default if merged as nil
			tmp = XMergeTable(tmp, defaultValue);
		end
		XCfgVar[varName] = tmp;
	end
end

function XCopyHierarchicalSetupCallbacks()
	local mapRules = XVar.currentMapInfo.mapRules;
	local mapName = XVar.currentMapInfo.mapName;
	local callbackNames = {
		"OnTimer",
		"OnNewMap",
	};
	if (not XSetup) then
		XSetup = {};
	end
	if (not XSetup.unknown) then
		XSetup.unknown = {};
	end
	if (not XSetup.unknown.unknown) then
		XSetup.unknown.unknown = {};
	end
	if (not XSetup.unknown.unknown.OnNewMap) then
		XSetup.unknown.unknown.OnNewMap = XSetupDefault.unknown.unknown.OnNewMap;
	end
	if (not XSetup.unknown.unknown.OnTimer) then
		XSetup.unknown.unknown.OnTimer = XSetupDefault.unknown.unknown.OnTimer;
	end

	if (not XSetup[mapRules]) then
		XSetup[mapRules] = {};
	end

	if (not XSetup[mapRules].unknown) then
		XSetup[mapRules].unknown = {};
	end
	if (not XSetup[mapRules].unknown.OnNewMap) then
		XSetup[mapRules].unknown.OnNewMap = XSetupDefault.known.unknown.OnNewMap;
	end
	if (not XSetup[mapRules].unknown.OnTimer) then
		XSetup[mapRules].unknown.OnTimer = XSetupDefault.known.unknown.OnTimer;
	end

	if (not XSetup[mapRules][mapName]) then
		XSetup[mapRules][mapName] = {};
	end
	if (not XSetup[mapRules][mapName].OnNewMap) then
		XSetup[mapRules][mapName].OnNewMap = XSetupDefault.known.known.OnNewMap;
	end
	if (not XSetup[mapRules][mapName].OnTimer) then
		XSetup[mapRules][mapName].OnTimer = XSetupDefault.known.known.OnTimer;
	end
	for i, callbackName in ipairs(callbackNames) do
		local callbackFn = ( ( (XSetup or {})[mapRules] or {})[mapName] or {})[callbackName];
		if (not callbackFn) then
			callbackFn = ( ( (XSetup or {})[mapRules] or {}).unkown or {})[callbackName];
		end
		if (not callbackFn) then
			callbackFn = ( ( (XSetup or {}).unknown or {}).unkown or {})[callbackName];
		end
		if (not callbackFn) then
			callbackFn = function(mapRules, mapName) end;
		end
		XCfgVar[callbackName] = callbackFn;
	end
end

function XSetupDirs()
	LogAlways("[AEGIS] Configuring folder structure...");
	local dir = XRuntimeVar.serverRootDir.."AEGIS/";
	XRuntimeVar.serverConfigDir = dir;
	XRuntimeVar.serverLogDir = dir.."ssm_logs/";
	XRuntimeVar.serverFlagDir = dir.."flags/";
	LogAlways("[AEGIS] Root directory: "..XRuntimeVar.serverRootDir);
    LogAlways("[AEGIS] Config directory: "..XRuntimeVar.serverConfigDir);
    LogAlways("[AEGIS] Log directory: "..XRuntimeVar.serverLogDir);
    LogAlways("[AEGIS] Flag directory: "..XRuntimeVar.serverFlagDir);
	LogAlways("[AEGIS] Folder structure configured!");
end


function XReload(forced)
	System.ExecuteCommand("dump_maps"); -- trigger filling of map paths
	XScanServerLog();
	local OldSetup = XSetup;
	local OldConfig = XConfig;

	XPrepareSetupTables();
	local configScript = "X"..AEGIS.NAME..".lua";
	local experimentalScript = "X"..AEGIS.NAME.."ExperimentalCode.lua";
	Script.UnloadScript(experimentalScript);
	Script.UnloadScript(configScript);
 	local dir = XRuntimeVar.serverConfigDir;
	if (not dir or (dir == "")) then
		LogAlways("[AEGIS] loading script %s", configScript);
		Script.LoadScript(configScript);
		if (XConfig and XConfig.useExperimentalConfigFile) then
			LogAlways("[AEGIS] loading script %s", experimentalScript);
			Script.LoadScript(experimentalScript);
		end
	else
		local fnLoadAndRun = function (file)
			assert(loadfile(file))();
		end;
		LogAlways("[AEGIS] loading file %s", dir..configScript);
		XDebugCall(fnLoadAndRun, dir..configScript);
		if (XConfig and XConfig.useExperimentalConfigFile) then
			LogAlways("[AEGIS] loading file %s", dir..experimentalScript);
			XDebugCall(fnLoadAndRun, dir..experimentalScript);
		end
	end
	if (not OldConfig) then
		if (XConfig and XConfig.validConfiguration) then
			LogAdmins("[AEGIS] configuration loaded successfully.");
			XVar.dirtyConfig = true;
		else
			LogAdmins("[AEGIS] CONFIGURATION LOADED WITH ERRORS.");
			XVar.dirtyConfig = true;
		end
	else
		if (XConfig and validConfiguration) then
			LogAdmins("[AEGIS] configuration was re-loaded successfully.");
			XVar.dirtyConfig = true;
		else
			LogAdmins("[AEGIS] NEW CONFIGURATION LOADED WITH ERRORS.");
			if (OldConfig and OldConfig.validConfiguration) then
				LogAdmins("[AEGIS] FALLING BACK TO PREVIOUSLY LOADED CONFIGURATION.");
				XSetup = OldSetup;
				XConfig = OldConfig;
			else
				LogAdmins("[AEGIS] USING CONFIGURATION ANYWAY - DONT HAVE A VALID FALLBACK.");
				XVar.dirtyConfig = true;
			end
		end
	end
	if (XVar.dirtyConfig) then
		math.randomseed(os.time());

		if (forced) then
			XScanServerLog();
			XResetConfig();
			XPrepareMap();
			XSafeCall(XCfgVar.OnNewMap, XVar.currentMapInfo.mapRules, XVar.currentMapInfo.mapName);
		end
	end
end


-- build a list of currently unused funnyNames
function XRebuildFunnyNames()
	local tmp = {};
	for i, n in ipairs(XCfgVar.funnyNames) do
		tmp[n] = 1;
	end
	local players = System.GetEntitiesByClass("Player");
	for i, p in ipairs(players) do
		tmp[p:GetName()] = nil;
		if (p.x_funnyName) then
			tmp[p.x_funnyName] = nil;
		end
	end
	local fn = {}
	for n, v in pairs(tmp) do
		table.insert(fn, n);
	end
	return fn;
end

-- identify the currently loaded map
function XResetConfig()
	if (g_gameRules.class) then
		if (XVar.lastRecognizedLevelLoading) then
			XVar.currentMapInfo = {
				mapRules = g_gameRules.class;
				mapName = string.lower(string.gsub(XVar.lastRecognizedLevelLoading, "%W", "_"));
			};
			LogAlways("[AEGIS] recognized map, using XSetup.%s.%s.*",
					XVar.currentMapInfo.mapRules or "NIL", XVar.currentMapInfo.mapName or "NIL");
		else
			XVar.currentMapInfo = {
				mapRules = g_gameRules.class;
				mapName = "unknown";
			};
			LogAlways("[AEGIS] unrecognized map, using XSetup.%s.unknown.*", g_gameRules.class);
		end
	else
		XVar.currentMapInfo = {
			mapRules = "unknown";
			mapName = "unknown";
		};
		LogAlways("[AEGIS] unrecognized game rules, using XSetup.unknown.unknown.*");
	end

	XCfgVar = {};
	-- merge in hierarchical overrides
	XMergeHierarchicalSetup();

	-- copy in callbacks
	XCopyHierarchicalSetupCallbacks();

	-- RESET VERIABLES in XVar :

	-- reset spamming
	XVar.nextSpamMessageTime = _time + XCfgVar.spamInitialDelay;
	XVar.nextSpamMessageIndex = 1;
	
	XVar.funnyNames = XRebuildFunnyNames();
	XVar.mapthenactive = false;

	-- reset abandon timer
	XVar.lastMapUseTime = _time;
	XVar.mapWasPopulated = nil;

	-- reset AutoMKC's selfTermination queue
	XVar.selfTerminationRequests = {};
end

function XGetPlayersCount()
	local count = 0;
	local ents = System.GetEntitiesByClass("Player");
	for i, e in ipairs(ents) do
		if (e.actor:GetSpectatorMode() == 0) then
			count = count + 1;
		end
	end
	return count;
end

function XSanitizePlayerName(name, previousFunnyName)
	local isFunnyName = false;
	local requireFunnyName = false;
	if (XCfgVar.useFunnyNamesForNomadsFeature) then
		if (string.find(string.lower(name), "nomad")) then
			requireFunnyName = true;
		end
	end
	if (XCfgVar.useFunnyNamesForCrypticsFeature) then
		local readable = string.len(string.gsub(name, "%W", ""));
		local total = string.len(name);
		local ratio = readable/total;
		if (ratio <= XCfgVar.crypticNameRatio) then
			requireFunnyName = true;
		end
	end
	if (requireFunnyName) then
		if (previousFunnyName) then
			name = previousFunnyName;
		else
			local t = #XVar.funnyNames;
			if (t == 0) then
				XVar.funnyNames = XRebuildFunnyNames();
				t = #XVar.funnyNames;
			end
			local i = math.random(t);
			name = "["..table.remove(XVar.funnyNames, i).."]";
		end
		isFunnyName = true;
	end
	if (XCfgVar.useSanitizeColoredNamesFeature) then
		name = string.gsub(name, "%$%$", "S");
		name = string.gsub(name, "%$.", "");
		name = string.gsub(name, "%$", "");
	end
	if (XCfgVar.useSanitizeSpaceInNamesFeature) then
		name = string.gsub(name, " ", "_");
	end
--	if (string.match(string.lower(name), "^%!*(.)*$")) then
    name = string.gsub(name, "^%!", "");

	return name, isFunnyName;
end

function XDumpPlayerNamingHistory(player)
	for i, tmp in ipairs(player.x_names) do
		LogAlways("[AEGIS]	on %-8s: '%s' --> '%s'", tmp.cause,
			string.gsub(tmp.originalName, "%$", "$$"),
			string.gsub(tmp.changedName, "%$", "$$"));
	end
end

--=============================================================================
-- Function: XServerWelcomeMessages
--
-- This function handles all welcome and info messages for players.
--
-- \param[in] player
-- The player the messages are sent to
--
--=============================================================================

function XServerWelcomeMessages(player)
        if (player.WelcomeMessage and not player.WelcomeMessageSent and (_time-player.WelcomeMessage)>10) and ((_time-player.WelcomeMessage)<20) then
                player.WelcomeMessageSent = true;
                if (XIsAdmin(player) and XCfgVar.useServerJoinWelcomeMessageAdmin and XCfgVar.serverJoinWelcomeMessageAdmin) then
                        if (player.actor:GetSpectatorMode()~=0) then
                                if (XCfgVar.serverJoinWelcomeMessageChatAdmin) then
                                       XSendChatToTarget(nil, player, XCfgVar.serverJoinWelcomeMessageChatAdmin);
                                end
                        else
                                XSendBigCenterMessageToPlayer(player,XFormat(XCfgVar.serverJoinWelcomeMessageAdmin,player:GetName()));
                                Script.SetTimer( 2000,function() 
                                        XSendBigCenterMessageToPlayer(player,XFormat(XCfgVar.serverJoinWelcomeMessageAdmin,player:GetName()));
                                end);
                        end
                                    
                elseif (not XIsAdmin(player)) and (XIsPremium(player)) and (XCfgVar.useServerJoinWelcomeMessagePremium and XCfgVar.serverJoinWelcomeMessagePremium) then
                        if (player.actor:GetSpectatorMode()~=0) then
                                if (XCfgVar.serverJoinWelcomeMessageChatPremium) then
                                        XSendChatToTarget(nil, player, XCfgVar.serverJoinWelcomeMessageChatPremium);
                                end
                        else
                                XSendBigCenterMessageToPlayer(player,XFormat(XCfgVar.serverJoinWelcomeMessagePremium,player:GetName()));
                                Script.SetTimer( 2000,function() 
                                        XSendBigCenterMessageToPlayer(player,XFormat(XCfgVar.serverJoinWelcomeMessagePremium,player:GetName()));
                                end);
                        end
                                    
                elseif (not XIsAdmin(player)) and (not XIsPremium(player)) and (XCfgVar.useServerJoinWelcomeMessage and XCfgVar.serverJoinWelcomeMessage) then
                        if (player.actor:GetSpectatorMode()~=0) then
                                if (XCfgVar.serverJoinWelcomeMessageChat) then
                                        XSendChatToTarget(nil, player, XCfgVar.serverJoinWelcomeMessageChat);
                                end
                        else
                                XSendBigCenterMessageToPlayer(player,XFormat(XCfgVar.serverJoinWelcomeMessage,player:GetName()));
                                Script.SetTimer( 2000,function() 
                                        XSendBigCenterMessageToPlayer(player,XFormat(XCfgVar.serverJoinWelcomeMessage,player:GetName()))
                                end);
                        end
                end
                
        elseif (player.WelcomeMessage and (_time-player.WelcomeMessage)>30) then
                player.WelcomeMessage = nil;
                if (XIsAdmin(player) and XCfgVar.useServerInfoWelcomeMessageAdmin and XCfgVar.serverInfoWelcomeMessageAdmin) then
                        if (player.actor:GetSpectatorMode()~=0) then
                                if (XCfgVar.serverInfoWelcomeMessageChatAdmin) then
                                        XSendChatToTarget(nil, player, XCfgVar.serverInfoWelcomeMessageChatAdmin);
                                end
                        else
                                XSendBigCenterMessageToPlayer(player, XCfgVar.serverInfoWelcomeMessageAdmin);
                                Script.SetTimer( 2000,function() 
                                       XSendBigCenterMessageToPlayer(player, XCfgVar.serverInfoWelcomeMessageAdmin);
                                end);
                        end
                                    
                 elseif (not XIsAdmin(player)) and (XIsPremium(player) and XCfgVar.useServerInfoWelcomeMessagePremium and XCfgVar.serverInfoWelcomeMessagePremium) then
                        if (player.actor:GetSpectatorMode()~=0) then
                                if (XCfgVar.serverInfoWelcomeMessageChatPremium) then
                                        XSendChatToTarget(nil, player, XCfgVar.serverInfoWelcomeMessageChatPremium);
                                end
                        else
                                XSendBigCenterMessageToPlayer(player, XCfgVar.serverInfoWelcomeMessagePremium);
                                Script.SetTimer( 2000,function() 
                                        XSendBigCenterMessageToPlayer(player, XCfgVar.serverInfoWelcomeMessagePremium);
                                end);
                        end
                                    
                 elseif (XCfgVar.useServerInfoWelcomeMessage and XCfgVar.serverInfoWelcomeMessage) then
                        if (player.actor:GetSpectatorMode()~=0) then
                                if (XCfgVar.serverInfoWelcomeMessageChat) then
                                        XSendChatToTarget(nil, player, XCfgVar.serverInfoWelcomeMessageChat);
                                end
                        else
                                XSendBigCenterMessageToPlayer(player, XCfgVar.serverInfoWelcomeMessage);
                                Script.SetTimer( 2000,function() 
                                        XSendBigCenterMessageToPlayer(player, XCfgVar.serverInfoWelcomeMessage);
                                end);
                        end
                 end
                 
                 player.WelcomeMessage = false;
        end
end

--=============================================================================
-- Function: XAEGISAntiCheat
--
-- This function handles all of the SSM Aegis anticheat. It is called in
-- InstantAciton.lua on each UpdatePings. It handles the timing, and what
-- functions are currently avaliable.
--
-- \param[in] frameTime
-- Time that the frame was produced, used for timer settings
--
--=============================================================================

function XAEGISAntiCheat(frameTime)
	if (not XVar.antiCheatTimer) then
		XVar.antiCheatTimer = 0;
	end
	XVar.antiCheatTimer = XVar.antiCheatTimer - frameTime;
	if (XVar.antiCheatTimer<=0) then

		XVar.antiCheatTimer = XVar.antiCheatTimer + XCfgVar.antiCheatTimer;
		if (XVar.antiCheatTimer<=0) then
			XVar.antiCheatTimer = XCfgVar.antiCheatTimer;
		end

	    local players = g_gameRules.game:GetPlayers();
		if (players) then
			for i,player in ipairs(players) do
				local skipChecks = false;
				if(player:IsDead() or player.actor:GetSpectatorMode()~=0 or (not player.myPrevPos)) then
				    player.exceptedTime  =   _time; --Allow for large movement between spectator location and spawn location
					skipChecks = true;
				end

				if (player.actorStats and player.actorStats.inAir and player.actorStats.inAir > 0) then
					skipChecks = true;
				end

				if(XIsVehicleNearby(player:GetWorldPos()) or (player.actorStats and player.actorStats.inAir and player.actorStats.inAir>0) or (player.exceptedTime and (_time - player.exceptedTime<3))) then
			skipChecks = true;
				end
				if (not skipChecks) then
					if (XCfgVar.antiSpeedHackEnabled) then
						XAEGISSpeedCheck(player);
					end
					if (XCfgVar.antiTeleportHackEnabled) then
					    XAEGISTeleportCheck(player);
					end
				end
		player.myPrevPos	=	player:GetWorldPos();
			end
		end
	end
end

--=============================================================================
-- Function: AEGIS:ProfileSpoofCheck
--
-- Checks and detects ProfileID spoofers, based on different issues.
-- Operational in SSM Aegis 1.2 Beta 5
--
-- \param[in] player
-- Player table to validate profileID
--=============================================================================
function AEGIS:ProfileSpoofCheck(player)
	if (player) then
	    local profile = XGetIdentityValue(player, "profile");
		local profileID = tonumber(profile);
	    local spoofed = false;
	    if (profileID<=1000 or profileID>=250000000) then
		spoofed = true;
			LogAdmins("[AEGIS] Detected Profile Spoof: %q Profile: %s",player:GetName(), profile);
		end
		if (spoofed) then
		XLogHackerDetails(player, "Profile spoofer");
			XDealWithHacker(player, "Profile spoofer");
		else
	    LogAdmins("[AEGIS] %q passed profile spoofer check. Profile: %s",player:GetName(), profile);
		end
	end
end



--=============================================================================
-- Function: XAEGISTeleportCheck
--
-- Checks and detects player teleportations based on flat distance. Uses
-- a coordinate-based system. If a player suddenly moves a large distance,
-- reguardless of speed, it will throw a detection.
-- Helps detect: Extreme speed mods, Extreme fly mods,
-- Ghost mod.
--
-- Operational SSM Aegis 1.2 Beta 5
--
-- \param[in] player
-- Player table to validate behavior
--
--=============================================================================

function XAEGISTeleportCheck(player)
	local x		=	(player:GetWorldPos().x - player.myPrevPos.x) * (player:GetWorldPos().x - player.myPrevPos.x)
	local y		=	(player:GetWorldPos().y - player.myPrevPos.y) * (player:GetWorldPos().y - player.myPrevPos.y)

	local distance	=	math.sqrt(x+y)
	if (XCfgVar.teleportDebug) then
	    LogAdmins("Current Distance: $6%s $1Limit: $4%s",tostring(distance),tostring(XCfgVar.teleportThreshold));
	end

	if(distance > XCfgVar.teleportThreshold) then
		if (not XIsAdmin(player)) then
			XLogHackerDetails(player, XFormat("Teleport hack (%s M)",tostring(distance)));
			XDealWithHacker(player, "Teleport hack");
		end
	end
end

--=============================================================================
-- Function: XAEGISSpeedCheck
--
-- Checks and detects player speed based on physics speed. Uses a physics-based,
-- C++ system. If a player is simply moving much faster than should be allowed,
-- it will throw a detection.
-- Helps detect: Speed mods
--
-- Operational SSM Aegis 1.2 Beta 5
--
-- \param[in] player
-- Player table to validate behavior
--
--=============================================================================

function XAEGISSpeedCheck(player)

end

--=============================================================================
-- Function: XAEGISFlyCheck
--
-- Checks for a fly mod. If a player has been in the air for a specific amount
-- of time and is still gaining altitude, while away form vehicles/other possible
-- explinations, it will throw a hack detect.
-- Helps detect: Fly mod, Ghost mod.
--
-- Not Currently Operational
--
-- \param[in] player
-- Player table to validate behavior
--
--=============================================================================

function XAEGISFlyCheck(player)
	--TODO: Create ;]
end

--=============================================================================
-- Function: XAEGISUnimitedEnergyCheck
--
-- Checks if the player's energy is currently draining correctly. If not, attempt
-- to force player's energy level to correct amount. Be wary of lag, as it may
-- cause false detections.
--
-- Not Currently Operational
--
-- \param[in] player
-- Player table to validate behavior
--
--=============================================================================

function XAEGISUnimitedEnergyCheck(player)
	--TODO: Create ;]
end

function AEGIS:PlayerIdentityBuilt(player)
    local profile = XGetIdentityValue(player, "profile");
    if (not AEGIS.PlayerInformationByProfile[profile]) then
	AEGIS.PlayerInformationByProfile[profile] = {};
	end

	AEGIS:PlayerInitialStats(player);
	if (XCfgVar.AEGISAntiCheat and XCfgVar.antiProfileSpoof) then
		AEGIS:ProfileSpoofCheck(player);
	end
end

function AEGIS:PlayerInitialStats(player)
	local scoresRestored = false;
	if (XCfgVar.usePersistantScores) then
		local profile = XGetIdentityValue(player, "profile");
		if (profile~="0") then
			if (AEGIS.PlayerInformationByProfile[profile] and AEGIS.PlayerInformationByProfile[profile].scores) then
				local scores = AEGIS.PlayerInformationByProfile[profile].scores;
				if (scores) then
					for key, score in pairs(scores) do
						g_gameRules.game:SetSynchedEntityValue(player.id, key, score);
						scoresRestored = true;
					end
				else
					scores = {};
					AEGIS.PlayerInformationByProfile[profile].scores = scores;
				end
			end
		end
	end
	--
	-- If the useStartingAverageRankFeature is turned on then we need to adjust
	-- the starting rank of a player based on the total rank of all players
	-- currently in the game up to the defined maximum starting rank.
	--
	if (not scoresRestored) then
		if (g_gameRules and g_gameRules.class == "PowerStruggle") then --Only PS needs to deal with average ranks
			if (XCfgVar.useStartingAverageRankFeature) then --Use AverageRanks to determine initial PP/CP
				local totalRank=0;
				local playerCount=0;
				local players=g_gameRules.game:GetPlayers();

				if (players) then
					--
					-- Calculate the total rank of all players on the server and the total
					-- player count as well. The player count could be determined by some
					-- other function but since we are already going through the entire
					-- player list it is just as easy to sum it up here. We do not count
					-- the entering player in the count or his/her rank in the sum.
					--
					for i,v in pairs(players) do
						local playerId=v.id;
						if (playerId and playerId~=player.id) then
							--
							-- This is not the entering player so add the players rank to the
							-- total rank and increment the player count.
							--
							totalRank=totalRank + g_gameRules:GetPlayerRank(playerId);
							playerCount=playerCount + 1;
						end
					end

					--
					-- Calculate average rank based on total rank and the player count and
					-- determine the new rank for the player based on the result and the
					-- max starting rank defined in the config. Note that this is only done
					-- if there is one or more players already in the game (i.e. player count
					-- and total rank are greater than 0).
					--
					if ((playerCount>0) and (totalRank>0)) then
						--
						-- The floor function is used to round the resulting average rank down
						-- to the next lowest integer value. The new rank is then determined
						-- using the min function to choose between the average rank or the
						-- max starting rank defined in the config.
						--
						local avgRank=math.floor(totalRank/playerCount);
						local newRank=math.min(avgRank, XCfgVar.maximumStartingAverageRank);

						--
						-- Now we get the new CP/PP values based on the new rank the player
						-- we be assigned. Once we have those values then the players CP, PP,
						-- and rank are set. Note that all three of these things MUST be done
						-- or else the changes will not stick when the player spawns for the
						-- first time.
						--
						local newCP=g_gameRules:GetRankCP(newRank);
						local newPP=g_gameRules:GetRankPP(newRank);

						g_gameRules:SetPlayerCP(player.id, newCP);
						g_gameRules:SetPlayerPP(player.id, newPP);
						g_gameRules:SetPlayerRank(player.id, newRank);
					end
				end
			else
				--
				-- The server is not using the auto rank adjust feature so give the player the
				-- default starting PP amount.
				--
				g_gameRules:SetPlayerPP(player.id, g_gameRules.ppList.START);
			end
		end
	end
end

-- Callback from timer
function XOnTimerTick(tid)
	XCrontabDaemon();

--------------------------
---------------- MESA RACE
--------------------------

if (RaceStarted and CheckPointActive) then local CheckPointCharlie = System.GetNearestEntityByClass(CheckPoint, 10, RaceCarType);
    if CheckPointCharlie ~= nil then local CheckPointVehicleName = CheckPointCharlie:GetName();
       if (CheckPointVehicleName=="RACER#1" or CheckPointVehicleName=="RACER#2" or CheckPointVehicleName=="RACER#3" or CheckPointVehicleName=="RACER#4") then local RacerId = CheckPointCharlie:GetDriverId();
           if (type(RacerId) == "userdata") then driver = System.GetEntity(RacerId);
              if (driver) then driverName = driver:GetName();
              XSendChatToAll(nil, ":: CHECKPOINT :: RACE-DRIVER %s leads the race!", driverName);
              g_gameRules:CreateExplosion(checkflare,checkflare,0,CheckPoint,g_Vectors.up,1,1,1,1,"explosions.flare.night_time_selfillum",1, 1, 1, 1);
              CheckPointActive = false;
              else return end
           else return end
       else return end
    else return end
end

if (RaceStarted) then
    local finishline = System.GetNearestEntityByClass(ChequeredFlag, 10, RaceCarType);
    if (not CheckPointActive and finishline ~= nil) then local WinningVehicleName = finishline:GetName();
       if (WinningVehicleName=="RACER#1" or WinningVehicleName=="RACER#2" or WinningVehicleName=="RACER#3" or WinningVehicleName=="RACER#4") then
          local finish = 5.0*math.ceil((_time-timestart)/5.0); laptime = (finish / 60);
          local RacerId = finishline:GetDriverId();
          if (type(RacerId) == "userdata") then driver = System.GetEntity(RacerId);
              if (driver) then driverName = driver:GetName(); local racerTeamId = g_gameRules.game:GetTeam(driver.id);
                 XSendChatToAll(nil, "RACE WON by %s for the %s team! ::: TIME : [ %s ] minutes! - WIN "..RaceAward.." POINTS!", driver:GetName(), X_TEAM_LABEL[racerTeamId], laptime);
                 g_gameRules.game:SendTextMessage(TextMessageBig, "<font size=\"32\"><font color=\"#f24545\"><b>*** RACE FINISH ***</b></font></font>", TextMessageToAllRemote);
                 XSendBigCenterMessageToPlayer(driver,"<font color=\"#FFFFFF\">[ </font><font color=\"#3cdd4f\"><b>WINNER</b></font><font color=\"#FFFFFF\"> ] - "..driver:GetName().." - TIME : [ <font color=\"#ffd800\">"..laptime.."</font> ]<font color=\"#FFFFFF\"> MINUTES!</font>");
                  g_gameRules:CreateExplosion(finishflare,weaponId,0,ChequeredFlag,g_Vectors.up,1,1,1,1,"explosions.flare.night_time_selfillum",1, 1, 1, 1);
                  g_gameRules:CreateExplosion(fireworks,weaponId,0,ChequeredFlag,g_Vectors.up,1,1,1,1,"misc.extremly_important_fx.celebrate",1, 1, 1, 1);
                  g_gameRules:CreateExplosion(finishflare,weaponId,0,{ 2218.3718,1782.6055,54.183601 },g_Vectors.up,1,1,1,1,"explosions.light.portable_light",1, 1, 1, 1);
                  g_gameRules:AwardPPCount(driver.id, RaceAward); RaceStarted = false;
                  if (laptime < recordtime) then recordtime = laptime; recordvehicle = RaceCarType;
                       recorddriver = driver:GetName();
                       FileHnd, Err = io.open("racerecord.lua","a+"); if (not FileHnd) then end;
                       FileHnd:write(XFormat("Driver: %s - RECORD TIME: %s - VEHICLE TYPE: %s\n", recorddriver, recordtime, recordvehicle)); FileHnd:close();
                   Script.SetTimer( 3500,function() XSendBigCenterMessageToPlayer(driver,"<font color=\"#FFFFFF\">[ </font><font color=\"#3db8e5\"><b>NEW RACE RECORD</b></font><font color=\"#FFFFFF\"> ] TIME [ <font color=\"#ffd800\">"..recordtime.."</font> ]<font color=\"#FFFFFF\"> MINUTES!</font>"); XSendChatToAll(nil, "NEW RACE RECORD ::: Congratulations %s ::: BONUS "..RaceAward.." POINTS!", recorddriver); g_gameRules:AwardPPCount(driver.id, RaceAward); g_gameRules:CreateExplosion(fireworks,weaponId,0,ChequeredFlag,g_Vectors.up,1,1,1,1,"misc.extremly_important_fx.celebrate",1, 1, 1, 1); g_gameRules:CreateExplosion(finishflare,weaponId,0,{ 2218.3718,1782.6055,54.183601 },g_Vectors.up,1,1,1,1,"explosions.light.portable_light",1, 1, 1, 1); end); else end
               else return end
           else return end
       else return end
    else return end
end

--------------------------
---------------- MESA RACE
--------------------------
	--add afk
	AEGIS.chatCommands.afk:update();
	local players = g_gameRules.game:GetPlayers();
	if (players) then
		for i, player in pairs(players) do
			do
				--Build identity information
				if (player and player.info and player.info.buildInfo==1 and XVar.logFile) then
					if (not player.statusRequests) then
						player.statusRequests = 0;
					end
					if (player.statusRequests>121) then
						System.RemoveEntity(player.id);
					elseif (player.statusRequests>120) then
						XKick(player:GetName(), 0, "Broken Connection");
					else
						if (_time - XVar.lastStatusScan > 5) then
							System.ExecuteCommand("status");
							if (XVar.punkbusterDetected) then
								System.ExecuteCommand("pb_sv_plist");
							end
							XVar.lastStatusScan = _time;
							player.statusRequests = player.statusRequests + 1;
						end
					end
				end
				if (XVar.punkbusterDetected and XVar.logFile) then
					if ((_time - XVar.lastStatusScan > 5) and player.info.pbUpdated <= 0) then
						System.ExecuteCommand("pb_sv_plist");
						XVar.lastStatusScan = _time;
					end
					if (player.info.pbUpdated == 0 and XCfgVar.usePunkbusterUpdateReminders) then
						XSendCenterMessageToPlayer(player, XCfgVar.pbUpdateReminderMessage1);
						XUpdatePBWarning(player);
					end
				end
			end

			do
				-- monitor name changes
				local name = player:GetName();
				if (not player.x_knownAs or (name ~= player.x_knownAs)) then
					LogAdmins("[AEGIS] %q has been renamed to %q through a user initiated name change.",
						string.gsub(player.x_knownAs or "", "%$", "$$"),
						string.gsub(name, "%$", "$$"));
					if (XCfgVar.sendChatOnRename) then
						XSendChatToAll(nil, "%q renamed himself to %q",	string.gsub(player.x_knownAs or "", "%$", "$$"),	string.gsub(name, "%$", "$$"));
					end
					if (not player.x_names) then
						player.x_names = { {
						originalName = name,
						changedName = name,
						cause = "join",
						} };
					end
					table.insert(player.x_names, {
						originalName = player.x_knownAs,
						changedName = name,
						cause = "request",
					});
					player.x_knownAs = name;
					if (XCfgVar.useSanitizeAfterNameChangesFeature) then
						local saneName, isFunnyName = XSanitizePlayerName(name, player.x_funnyName);
						if (saneName ~= name) then
							LogAdmins("[AEGIS] %q has been sanitized to %q after a user initiated name change.",
								string.gsub(name, "%$", "$$"),
								string.gsub(saneName, "%$", "$$"));
							g_gameRules.game:RenamePlayer(player.id, saneName);
							table.insert(player.x_names, {
								originalName = name,
								changedName = saneName,
								cause = "sanitize",
							});
							player.x_knownAs = saneName;
							if (isFunnyName) then
								player.x_funnyName = saneName;
							end
						end
					end
					if (XIsAdmin(player)) then
						player.x_skipNameHistory = true;
					end
					-- dump the naming history for this player
					if (not player.x_skipNameHistory) then
						XDumpPlayerNamingHistory(player);
					end
				end
			end
			-- check and set admin mode
			do
				if (XIsProtected(player)) then
					player.actor:SetNanoSuitEnergy(200)
					g_gameRules.game:SetInvulnerability(player.id, true, g_gameRules.INVULNERABILITY_TIME);
					player.x_startInvulnerabilityTime = _time;
			        if (XCfgVar.adminModeReminder) then
				            if (not player.x_ProtectInfoSent or (_time-player.x_ProtectInfoSent)>5) then
                                    XSendBigCenterMessageToPlayer(player, "<font color=\"#ff0000\">!!! Admin Mode !!!</font>");
                                    player.x_ProtectInfoSent = _time;
                            end
                    end
				end
				if (XCfgVar.useAdminImposterProtectionFeature) then
					if (XIsImposter(player)) then
						player.actor:SetNanoSuitEnergy(0)
						if (not player.x_imposterWarningSent or (_time-player.x_imposterWarningSent)>XCfgVar.adminImposterWarningInterval) then
							XSendCenterMessageToPlayer(player, XCfgVar.adminImposterWarning, XCfgVar.adminTag);
							XSendServerMessageToPlayer(player, XCfgVar.adminImposterWarning, XCfgVar.adminTag);
							player.x_imposterWarningSent = _time;
						end
					end
				end
			end

			-- check and handle penalty box
			local profile = XGetIdentityValue(player, "profile");
			if (profile~="0") then
				if (AEGIS.PlayerInformationByProfile[profile] and AEGIS.PlayerInformationByProfile[profile].PenaltyBox) then
					local pbox = AEGIS.PlayerInformationByProfile[profile].PenaltyBox;
					if (_time-pbox.startTime>pbox.duration) then
						-- Allows player to live, and resets inventory.
						XReleasePenaltyBox(player:GetName(), true);
					else
						local left = pbox.duration - 5.0*math.ceil((_time-pbox.startTime)/5.0);
						player.actor:SetNanoSuitEnergy(0);
						XSendCenterMessageToPlayer(player, "PENALTY BOX [%3ds left]: %s", left, pbox.message);
					end
				end
			end

                        -- Player Welcome Messages
                        XServerWelcomeMessages(player);

			-- check and act on high ping
			if (XCfgVar.useHighPingFeature) then
				if (player and player.actor:GetChannel()) then
					local ping = math.floor((g_gameRules.game:GetPing(player.actor:GetChannel()) or 0)*1000+0.5);
					if (not player.x_pingSamples) then
						player.x_pingSamples = {};
					end
					while (#player.x_pingSamples<XCfgVar.highPingTotalSamples) do table.insert(player.x_pingSamples, 1, 0); end;
					table.insert(player.x_pingSamples, 1, ping);
					while (#player.x_pingSamples>XCfgVar.highPingTotalSamples) do table.remove(player.x_pingSamples); end;
					local above = 0;
					for i, sample in ipairs(player.x_pingSamples) do
						if (sample>XCfgVar.highPingTimeThreshold) then
							above = above + 1;
						end
					end
					player.x_pingSamplesTotal = #player.x_pingSamples;
					player.x_pingSamplesAbove = above;
					player.x_pingSampleCurrent = ping;
					for i, exception in pairs(XCfgVar.highPingExceptions) do
						if (string.find(string.lower(player:GetName()), string.lower(exception))) then
							player.x_pingExcepted = true;
						end
					end
					if (above > XCfgVar.highPingSamplesToKick) then
						--Todo: Remove x_pingExcepted Global, make it a function like XIsAdmin
						if ((not XIsAdmin(player)) and (not XIsPremium(player)) and (not player.x_pingExcepted)) then
							local name = player:GetName();
							if (XCfgVar.sendChatOnPingKick) then
								XSendChatToAll(nil, "%s has been kicked for high ping ( > %d)", player:GetName(), XCfgVar.highPingTimeThreshold);
							end
							XKick(player:GetName(), 0, XFormat("High Ping (>%d)",XCfgVar.highPingTimeThreshold));
						end
						player.x_pingSamples = {}; -- avoid mass spam if kick fails
					elseif (above > XCfgVar.highPingSamplesToInform) then
						if (not player.x_highPingWarningSent and not player.x_pingExcepted) then
							if (XCfgVar.sendChatOnPingKick) then
								XSendChatToAll(nil, "WARNING! %s This server kicks for high ping! (Ping > %d)", player:GetName(), XCfgVar.highPingTimeThreshold);
							end
							XSendServerMessageToPlayer(
									player,
									XCfgVar.highPingWarningMessage,
									XCfgVar.highPingTimeThreshold);
							XSendCenterMessageToPlayer(
									player,
									XCfgVar.highPingWarningMessage,
									XCfgVar.highPingTimeThreshold);
							player.x_highPingWarningSent = true;
							-- XPing(); -- also dump the stats for the admins
						end
					elseif (above == 0) then
						player.x_highPingWarningSent = nil;
					end
				end
			end
		end
		if(XVar.vote.InProgress) then

			local voteTimeRemaining		=	math.floor(XCfgVar.voteTime -(_time - XVar.vote.StartTime));
			local playerCount 			=	g_gameRules.game:GetPlayerCount(true);

			if(#XVar.vote.Recieved == playerCount or voteTimeRemaining <= 0) then

				if(XVar.vote.Yes > (playerCount / 100 * XCfgVar.votePercentage)) then
                                  if (votemapcall) then
                                  	  	votemapcall = false;
                                    	XVar.mapthenactive = false;
                                    	System.ExecuteCommand(XVar.vote.gamerules);
                                   		if (XVar.vote.Map) then
                                     		System.ExecuteCommand("net_mapDownloadURL http://cobra-clan.de/autodownload/"..XVar.vote.Map..".zip");
                                   		end
                                    	System.ExecuteCommand(XVar.vote.roundtime);
                                   		if (XVar.vote.scorelimit) then
                                     	System.ExecuteCommand(XVar.vote.scorelimit);
                                   		end
                                    	System.ExecuteCommand(XVar.vote.Command);
                                   elseif (votenextmap) then                   
                                         votenextmap = false;
                                         XVar.mapthenactive = false;
                                         System.ExecuteCommand(XVar.vote.Command);
                                   else
                                        if (XVar.vote.Map) then
                                     		System.ExecuteCommand("net_mapDownloadURL http://cobra-clan.de/autodownload/"..XVar.vote.Map..".zip");
                                   		end
                                    	System.ExecuteCommand(XVar.vote.Command);
                                  end

				else
					XSendBigCenterMessage("Vote for \""..XVar.vote.Command.."\" failed");
				end

				XVar.vote.InProgress 	=	nil;
				XVar.vote.StartTime 	= 	nil;
				XVar.vote.Recieved		=	nil;
				XVar.vote.Command		=	nil;
				XVar.vote.Yes			=	nil;
				XVar.vote.No			=	nil;

			end

		end
	end

	if (XRuntimeVar.isMassKillCountermeasureEnabled) then
		XSendCenterMessageToAllRemote("** ADMIN: Mass Kill Countermeasure ACTIVE  --  Stop playing until the cheater leaves. Thank you! **");
	elseif (#XVar.adminMessages>0) then
		XSendCenterMessageToAllRemote("** ADMIN: %s **", XVar.adminMessages[1])
		XVar.adminMessageRepeatCount = XVar.adminMessageRepeatCount - 1;
		if (XVar.adminMessageRepeatCount <= 0) then
			table.remove(XVar.adminMessages, 1);
			if (#XVar.adminMessages>0) then
				XVar.adminMessageRepeatCount = XCfgVar.adminMessageDuration;
			end
		end
	end

	if (XCfgVar.spamMessages) then
	      XSpamMessages();
    end	
    
	if (XCfgVar.useAdminSpamFeature) then
		if (XVar.adminSpamMsg) then
			if ((not XVar.adminSpamRepeatCount) or (XVar.adminSpamRepeatCount <= 0)) then
				XAdminMessage(XVar.adminSpamMsg);
				XVar.adminSpamRepeatCount = XCfgVar.adminSpamInterval;
			end
			XVar.adminSpamRepeatCount = XVar.adminSpamRepeatCount - 1;
		else
			XVar.adminSpamRepeatCount = 0;
		end
	end

	if (XIsServerSideGameReady()) then
		if (XIsMapPopulated()) then
			XVar.lastMapUseTime = _time;
			XVar.mapWasPopulated = true;
		end
		if (XCfgVar.useAlternateLevelRotation and (XCfgVar.mapAbandonTime>0) and ((_time-XVar.lastMapUseTime)>XCfgVar.mapAbandonTime)) then
			XVar.lastMapUseTime = _time;
			XVar.mapWasPopulated = false;
			g_gameRules:XtendedNextLevel();
		end

		if (XCfgVar.useResurrectTurretsFeature) then
			XResurrectTurrets();
		end

		XSafeCall(XCfgVar.OnTimer, XVar.currentMapInfo.mapRules, XVar.currentMapInfo.mapName);
	end
end

-- ===============================================
-- Scanning and interpreting the server log
-- ===============================================

-- retrieves information otherwise not available to Lua

function XScanServerLog()
	if (not XVar.logFile) then
		local dir = XRuntimeVar.serverRootDir;
		if (not dir) then
			dir = "";
		end
		local logFileName = dir.."server.log";
		local logFile, err = io.open(logFileName, "r");
		if (not logFile) then
			if (not XVar.lastLogFileErrorTime or (_time-XVar.lastLogFileErrorTime>30)) then
				XVar.lastLogFileErrorTime = _time;
				LogAdmins("[SSM:LOG] opening %s for reading failed: %s", logFileName, err);
			end
			return;
		end
		XVar.logFile = logFile;
		LogAlways("[SSM:LOG] opened %s for reading and monitoring", logFileName);
	end
	if (XVar.logFile) then
		for fullline in XVar.logFile:lines() do
			-- remove possibly preceeding timestamp
			local timestamp, line = string.match(fullline, "^(%<%d%d:%d%d:%d%d%>)%s(.*)$");
			if (not timestamp) then
				line = fullline;
			end
			-- now line only contains the logged msg regardless of log_IncludeTime CVar.

			-- only process lines that have not been created as debug output from herein
			-- which essentially means only lines without the [SSM:LOG] tag]
			if (not string.match(line, "^%[SSM:LOG%]")) then
				if (not XVar.punkbusterDetected) then
					XScanForPBDetected(line);
				end
				if (XVar.punkbusterDetected) then
					XScanForPBStatus(line);
				end
				XScanForStatusLine(line);
				XScanForLevelLoading(line);
				XScanForDumpMaps(line);
				if(not XDllLoaded) then
				    XScanForChatMessages(line);
                end

				if (XScanForCSMExtension) then
					XScanForCSMExtension(line);
				end
				if (XScanForExperimental) then -- callback for experiments
					XDebugCall(XScanForExperimental, line);
				end
			end
		end
--		LogInfo("[LOG] processed %d log lines", count);
	end
end

function XScanForLevelLoading(line)
	local levelPath = string.match(line, "^%*LOADING: Level%s+(%S+)%s+loading time: .* seconds$");
	if (levelPath) then
		XVar.prevRecognizedLevelLoading = XVar.lastRecognizedLevelLoading;
		XVar.lastRecognizedLevelLoading = levelPath;
		if (XVar.prevRecognizedLevelLoading == XVar.lastRecognizedLevelLoading) then
			XVar.numberOfLevelRepeats = (XVar.numberOfLevelRepeats or 0) + 1;
		else
			XVar.numberOfLevelRepeats = 0;
		end
		LogAlways("[SSM:LOG] recognized level loading message: %s [%d]", levelPath, XVar.numberOfLevelRepeats);
	end
end

function XScanForDumpMaps(line)
	local mapPath = string.match(string.lower(line), "^  ([/%w_]+) %[%$9[/%w_]+%$o%]$");
	if (mapPath) then
		if (not XVar.mapPaths) then
			XVar.mapPaths = {};
		end
		local map = string.lower(mapPath);
		if (not XVar.mapPaths[map]) then
			XVar.mapPaths[map] = map;
			LogAlways("[AEGIS] added recognizable map path: %s", map);
		end
	end
end
function XScanForPBDetected(line)
	local serverVersion, clientAVersion, clientBVersion = string.match(line,
	".*PunkBuster Server: PunkBuster Server %((v%d+.%d+) %| (%w+) (%w+.%w+)%) Enabled");
	if (serverVersion) then
		XVar.punkbusterDetected = true;
		LogAdmins("[SSM:LOG] PunkBuster Detected running server version: %s", serverVersion);
		System.ExecuteCommand("pb_sv_updategrace 4800"); --Give them additional time to update.
	end
end

function XScanForStatusLine(line)
	local name, channelId, hostname, port, ping, state, profile = string.match(line,
		"^name: (.*)  id: (%d+)  ip: (.*):(%d+)  ping: (%d+)  state: (%d+) profile: (%d+)");
	if (name) then
		channelId = tonumber(channelId);
		local player = g_gameRules.game:GetPlayerByChannelId(channelId);
		if (player) then
			local info = {
				name = name,
				channel = channelId,
				host = hostname,
				port = port,
				profile = profile,
				buildInfo = 0,
			};
			local profile = XGetIdentityValue(player, "profile");
			if (profile == "0") then --Their identity is not yet built.
				XWriteIdentityRecordForPlayer(player, info);--Profile has been built now.
				profile = XGetIdentityValue(player, "profile");
				AEGIS:PlayerIdentityBuilt(player);
			end
			player.statusRequests = nil;
		end
	end
end

function XScanForPBStatus(line)
	local slot, guid, ip, port, status, power, auth, SS, os, name = string.match(line,
		'.*PunkBuster Server: (%d+)%s+(%x+).* (.*):(%d+) (%a+)%s+(%d+) (%d%.%d) (%d+) (.*) "(.*)"');
	if (name) then
		local player = XSimplePlayerForName(name, "");
		if (player) then
			local info = {
				slot = slot,
				guid = guid,
				ip = ip,
				port = port,
				status = status,
				power = power,
				os = os,
				name = name,
			};
			XWriteIdentityRecordForPlayer(player, info);
			if (status == "OK") then --Change to OK
				player.info.pbUpdated = 2;
			elseif (status == "INIT") then -- Change to INIT
				if ((not player:IsDead()) and (player.actor:GetSpectatorMode() == 0)) then
					player.info.pbUpdated = 0;
					if (XCfgVar.usePunkbusterUpdateReminders) then
						XUpdatePBWarning(player);
					end
				end
			elseif (status == "UPDT" and player.info.pbUpdated == 0) then
				XSendChatToTarget(nil, player, "Thank you for updating PB. Have a great day!");
				player.info.pbUpdated = 1;
			end
		end
	end
end

function XScanForChatMessages(line)
	if (XVar.scanForChatArmed) then
		-- any code error from here until armed flag is cleared
		-- will hang the server, so we clear the armed flag at first!!
		XVar.scanForChatArmed = false;
		local chatMsg = {
			time = _time;
			sender = XVar.scanForChatSender;
			target = XVar.scanForChatTarget;
			text = string.sub(line, 4);
		};
		XVar.scanForChatSender = nil;
		XVar.scanForChatTarget = nil;

		if ((chatMsg.target == "Team tan") or (chatMsg.target == "Team black")) then
			chatMsg.type = ChatToTeam;
			local player = XGetPlayerByName(chatMsg.sender);
			if (player) then
				local teamId = g_gameRules.game:GetTeam(player.id);
				XSendConsoleMessageToTeamAndAdmins(teamId, "$9[CHT] $8%s$9 to $1TEAM$9: $5%s", chatMsg.sender, chatMsg.text);
				local muteUnknownCommand = false;
				if (XCfgVar.useChatResponses) then
					muteUnknownCommand = XScanForChatResponse(player, chatMsg);
				end
				if (XCfgVar.useChatCommands) then
					XScanForChatCommand(player, chatMsg, muteUnknownCommand);
				end
			end
			LogAlways("[CHT] %s to %s: %s", chatMsg.sender, chatMsg.target, chatMsg.text);
			table.insert(XVar.chatMessages, chatMsg);
		elseif (chatMsg.target == "ALL") then
			chatMsg.type = ChatToAll;
			local player = XGetPlayerByName(chatMsg.sender);
			if (player) then
				XSendConsoleMessageToAll("$9[CHT] $8%s$9 to $1ALL$9: $5%s", chatMsg.sender, chatMsg.text);
				local muteUnknownCommand = false;
				if (XCfgVar.useChatResponses) then
					muteUnknownCommand = XScanForChatResponse(player, chatMsg);
				end
				if (XCfgVar.useChatCommands) then
					XScanForChatCommand(player, chatMsg, muteUnknownCommand);
				end
			end

			LogAlways("[CHT] %s to %s: %s", chatMsg.sender, chatMsg.target, chatMsg.text);
			table.insert(XVar.chatMessages, chatMsg);

		elseif (chatMsg.target == "<unknown>") then
			local sender = XGetPlayerByName(chatMsg.sender);
			if (not sender) then
				LogAlways("[AEGIS] unrecognized incoming chat message: unidentifiable sender [%s]", chatMsg.sender);
			else
				LogAlways("[AEGIS] unrecognized incoming chat message: unmatched text [%s]", chatMsg.text);
			end
		else
			if (chatMsg.sender ~= XCfgVar.ChatEntityName) then -- SSM Already logged it's own chat.
				chatMsg.type = ChatToTarget;
				XSendConsoleMessageToAll("$9[CHT] $8%s$9 to $8%s$9: $5%s", chatMsg.sender, chatMsg.target, chatMsg.text);
				LogAlways("[CHT] %s to %s: %s", chatMsg.sender, chatMsg.target, chatMsg.text);
				table.insert(XVar.chatMessages, chatMsg);
			end
		end
		while (XVar.chatMessages and (#XVar.chatMessages>XCfgVar.chatMessagestoHold)) do
			table.remove(XVar.chatMessages, 1);
		end
	else
		local sender, tgt = string.match(line, "^CHAT (.*) to (.*):$");
		if (sender) then
			XVar.scanForChatArmed = true;
			XVar.scanForChatSender = sender;
			XVar.scanForChatTarget = tgt;
		end
	end
end

function XScanForChatResponse(player, chatMsg)
	if (player) then
		for i, currentTrigger in pairs(XCfgVar.ChatTriggers) do
			local check = false;
			for i,triggerWord in ipairs(currentTrigger.Triggers) do
				if string.match(string.lower(chatMsg.text), string.lower(triggerWord)) then
					check = true;
					break;
				end
			end
			if (check) then
				for i, response in pairs(currentTrigger.Responses) do
					XSendChatToTarget(nil, player, response);
				end
				if (string.match(chatMsg.text, "^!.*$")) then
					return true;
				else
					return false;
				end
			end
		end
	end
end

-- =============================================================================
-- Function: XScanForChatCommand
--
-- This function determines if a chat command was issued by looping through all
-- the defined chat commands and comparing the command word to the chat message.
--
-- \param[in] player
-- The entity of the player that sent the message.
--
-- \param[in] chatMsg
-- Structure containing the data associated with the chat message.
-- =============================================================================

function XScanForChatCommand(player, chatMsg, muteUnknownCommand)
	if (player) then
		local command = string.match(string.lower(chatMsg.text), "^!(%w+).*"); --Lets make the command lowercase!
		if (command) then
			if (AEGIS.chatCommands[command]) then
				if (AEGIS.chatCommands[command].adminOnly) then
					if (not XCfgVar.useAdminChatCommands) then
						Script.SetTimer(2, function() XSendChatToTarget(nil, player, "This command is disabled."); end);
						return;
					else
						if (XIsAdmin(player)) then
							Script.SetTimer(2, function() AEGIS.chatCommands[command]:func(player, chatMsg); end);
						else
							Script.SetTimer(2, function() XSendChatToTarget(nil, player, '"!'..command..'" is an admin-only command.'); end);
						end
					end
				-- 1.4 Premium Handling
				elseif (AEGIS.chatCommands[command].premiumOnly) then
					if (not XCfgVar.usePremiumChatCommands) then
						Script.SetTimer(2, function() XSendChatToTarget(nil, player, "This command is disabled."); end);
						return;
					else
						if (XIsPremium(player) or XIsAdmin(player)) then
							Script.SetTimer(2, function() AEGIS.chatCommands[command]:func(player, chatMsg); end);
						else
							Script.SetTimer(2, function() XSendChatToTarget(nil, player, '"!'..command..'" is an premium-only command.'); end);
						end
					end
                                else
		                 Script.SetTimer(2, function() AEGIS.chatCommands[command]:func(player, chatMsg); end);
				end
				return true;
			else
				if (not muteUnknownCommand) then
					Script.SetTimer(2, function() XSendChatToTarget(nil, player, "Unknown command: %q", command); end);
				end
			end
		end
	end
end

-- =============================================================================
-- Function: XUpdatePBWarning
--
-- This function
--
-- \param[in] player
-- The entity of the player that issued the command.
-- =============================================================================
function XUpdatePBWarning(player)
	if (not player.info.pbUpdated == 0) then
		return;
	end
	if (not player.needsToUpdatePBTime) then
		player.needsToUpdatePBTime = 0;
	end
	if (_time - player.needsToUpdatePBTime > 3) then
		XSendChatToTarget(nil, player, XCfgVar.pbUpdateReminderMessage1);
		XSendChatToTarget(nil, player, XCfgVar.pbUpdateReminderMessage2);
		XSendConsoleMessageToPlayer(player, XCfgVar.pbUpdateConsoleMessage)
		XSendConsoleMessageToPlayer(player, XCfgVar.pbUpdateConsoleMessage)
		XSendServerMessageToPlayer(player, XCfgVar.pbUpdateReminderMessage1);
		XSendServerMessageToPlayer(player, XCfgVar.pbUpdateReminderMessage2);
		g_gameRules.game:SendTextMessage(TextMessageError, XFormat(XCfgVar.pbUpdateReminderMessage1), TextMessageToClient, player.id);
		g_gameRules.game:SendTextMessage(TextMessageError, XFormat(XCfgVar.pbUpdateReminderMessage2), TextMessageToClient, player.id);
		player.needsToUpdatePBTime = _time;
	end
end

-- ===============================================
-- Identity Handling
-- ===============================================

function XGetIdentityRecordForPlayer(player)
	--[[
	player.info.slot
	player.info.guid
	player.info.ip
	player.info.port
	player.info.status
	player.info.power
	player.info.os
	player.info.name
	player.info.channel
	player.info.host
	player.info.profile
	player.info.buildInfo
	player.info.pbUpdated
	]]--
end
--name, channelId, hostname, port, ping, state, profile
function XWriteIdentityRecordForPlayer(player, info)
	if (player) then
		XMergeTable(player.info, info);
	end
end

function XSpamMessages()
	if (_time > XVar.nextSpamMessageTime) then
    	if (XVar.nextSpamMessageIndex > #XCfgVar.spamMessages) then
    		XVar.nextSpamMessageTime = _time + XCfgVar.spamDelay;
    		XVar.nextSpamMessageIndex = 1;
    	end
    	local players = g_gameRules.game:GetPlayers();
    	if (players) then
    		local msg = XCfgVar.spamMessages[XVar.nextSpamMessageIndex];
            if (XCfgVar.spamMessagesunderTimefield) then   
                g_gameRules.game:SendTextMessage(TextMessageInfo, msg, TextMessageToAllRemote);
                if (XVar.mapthenactive) then
                    Script.SetTimer( 6000,function() 
                        g_gameRules.game:SendTextMessage(TextMessageInfo, XVar.mapthenactive, TextMessageToAllRemote);
                    end);
                end
            else
                g_gameRules.game:SendTextMessage(TextMessageServer, msg, TextMessageToAllRemote);
                if (XVar.mapthenactive) then
                    Script.SetTimer( 6000,function() 
                        g_gameRules.game:SendTextMessage(TextMessageServer, XVar.mapthenactive, TextMessageToAllRemote);
                    end);
                end
            end
    	end
    	XVar.nextSpamMessageTime = _time + XCfgVar.spamDelay;
    	XVar.nextSpamMessageIndex = XVar.nextSpamMessageIndex + 1;
    end
end



-- ===============================================
-- The Crontab Feature
-- ===============================================
function XCrontabDaemon()
	if (not XCfgVar.useCrontabFeature) then
		return;
	end
	local tt = os.date("*t");
	local now = {tt.min, tt.hour, tt.day, tt.month, tt.wday-1};
	if (not XVar.lastCronCheck) then
		XVar.lastCronCheck = now;
	end
	if (not XArrayEquals(now, XVar.lastCronCheck)) then
		LogInfo("[AEGIS] new crontab check: %s %s %s %s %s", now[1], now[2], now[3], now[4], now[5]);
		XVar.lastCronCheck = now;

		for i, entry in ipairs(XCfgVar.crontab) do
			local spec = XStringSplit(entry, "[	]+", 6);
			local cmd = spec[6];

			local matchMin   = XCrontabMatch(now[1], spec[1], 0, 59, false, entry);
			local matchHour  = XCrontabMatch(now[2], spec[2], 0, 23, false, entry);
			local matchDay   = XCrontabMatch(now[3], spec[3], 1, 31, false, entry);
			local matchMonth = XCrontabMatch(now[4], spec[4], 1, 12, false, entry);
			local matchDOW   = XCrontabMatch(now[5], spec[5], 0,  7, true , entry);
			if (matchMin and matchHour and matchMonth and (matchDay or matchDOW)) then
				LogInfo("[AEGIS] matched crontab entry: %s %s %s %s %s", spec[1], spec[2], spec[3], spec[4], spec[5]);
				LogAlways("[AEGIS] crontab executing: %s", cmd);
				local fc = string.sub(cmd, 1, 1);
				if (fc == "!") then
					local status = XSafeCall(os.execute, string.sub(cmd, 2));
					if (status) then
						LogInfo("[AEGIS] execution of '%s' returned: %s", cmd, tostring(status));
					end
				elseif (fc == "#") then
					local chunk, error = loadstring(string.sub(cmd, 2), cmd);
					if (chunk) then
						XDebugCall(chunk);
					else
						LogAlways("[AEGIS] Error: %s", tostring(error));
					end
				else
					System.ExecuteCommand(cmd);
				end
			end
		end
	end
end

function XCrontabMatch(now, spec, from, to, overlap, entry)
	if (overlap and ((now == from) or (now == to))) then
		return XCrontabMatch(from, spec, from, to, false, entry)
			or XCrontabMatch(to  , spec, from, to, false, entry);
	end
	local ranges = XStringSplit(spec, ", ");
	for i, range in ipairs(ranges) do
		if (range == "*" and (from <= now) and (now <= to)) then
			return true;
		end
		do
			local step = string.match(range, "^%*%/(%d+)$");
			if (step) then
				step = tonumber(step);
				if (step>0) then
					for v = from, to, step do
						if (now == v) then
							return true;
						end
					end
				end
			end
		end
		do
			local rfrom, rto, step = string.match(range, "^(%d+)-(%d+)%/(%d+)$");
			if (step and tonumber(step) and (tonumber(step)>0)) then
				rfrom = tonumber(rfrom);
				rto = tonumber(rto);
				step = tonumber(step);
				if ((step>0) and (rfrom <= rto) and (from <= rfrom) and (rto <= to)) then
					for v = rfrom, rto, step do
						if (now == v) then
							return true;
						end
					end
				end
			end
		end
		do
			local rfrom, rto = string.match(range, "^(%d+)-(%d+)$");
			if (rto) then
				rfrom = tonumber(rfrom);
				rto = tonumber(rto);
				if ((rfrom <= rto) and (from <= rfrom) and (rto <= to)) then
					for v = rfrom, rto do
						if (now == v) then
							return true;
						end
					end
				end
			end
		end
		do
			local rnow = string.match(range, "^(%d+)$");
			if (rnow) then
				rnow = XClampInteger(tonumber(rnow), from, to);
				if ((from <= rnow) and (rnow <= to) and (rnow == now)) then
					return true;
				end
			end
		end
	end
	return false;
end


-- ==========================================
-- Weapon/Hit Lookups
-- ==========================================

function XLookupOriginalWeaponDamage(weaponType, hit)
	local tmp = weaponType;
	while (true) do
		if (tmp == "HACK") then
			return 1, true;
		end
		local value = XCfgVar.weaponDamage[tmp];
		if (not value or ((type(value) ~= "number") and (type(value) ~= "string"))) then
			if ((tmp == weaponType) and hit.weapon and hit.weapon.weapon) then
				value = hit.weapon.weapon:GetDamage();
			end
			if (not value) then
				value = 8000;
			end
			local file, err = io.open("SSM.log", "a+");
			if (file) then
				file:seek("end");
				file:write(XFormat("[AEGIS] missing damage info for %s( = >%s) = %g\n", weaponType, tmp, value));
				file:close();
			end
			LogAlwaysWarning("[AEGIS] missing damage info for %s( = >%s) = %g..", weaponType, tmp, value);
			XCfgVar.weaponDamage[tmp] = value;
			return value, false;
		end
		if (type(value) == "number") then
			return value, false;
		end
		-- redirected
		tmp = value;
	end
end

function XLookupModdedWeaponDamage(weaponType, targetType, damageKey)
	local redirected = false;
	local tmpw = weaponType;
	while (true) do
		if (tmpw == "HACK") then
			return 1, true;
		end
		local value = nil;

		local tmpt = targetType;
		while (true) do
			local tblt = XCfgVar.weaponDamageTo[tmpt];
			if (tblt) then
				if (damageKey) then
					if (tblt[damageKey]) then
						value = tblt[damageKey][tmpw];
					end
				else
					value = tblt[tmpw];
				end
			end
			if (value and ((type(value) == "number") or (type(value) == "string"))) then
				break;
			end
			tmpt = (XVehicleInfo[tmpt] or {}).Parent;
			redirected = true;
			if (not tmpt) then
				value = XCfgVar.weaponDamage[tmpw];
				if (not value or ((type(value) ~= "number") and (type(value) ~= "string"))) then
					value = 8000;
					local file, err = io.open("SSM.log", "a+");
					if (file) then
						file:seek("end");
						file:write(XFormat("[AEGIS] missing damage info for %s( = >%s) = %g\n", weaponType, tmpw, value));
						file:close();
					end
					LogAlwaysWarning("[AEGIS] missing damage info for %s( = >%s) = %g..", weaponType, tmpw, value);
					XCfgVar.weaponDamage[tmpw] = value;
				end
				break;
			end
		end

		if (type(value) == "number") then
			if (redirected) then
				-- place a shortcut
				if (not XCfgVar.weaponDamageTo[targetType]) then
					XCfgVar.weaponDamageTo[targetType] = {};
				end
				if (damageKey) then
					if (not XCfgVar.weaponDamageTo[targetType][damageKey]) then
						XCfgVar.weaponDamageTo[targetType][damageKey] = {};
					end
					XCfgVar.weaponDamageTo[targetType][damageKey][weaponType] = value;
				else
					XCfgVar.weaponDamageTo[targetType][weaponType] = value;
				end
			end
			return value, false;
		end
		-- redirected
		tmpw = value;
		redirected = true;
	end
end


-- retrieve weapon damage information about the current hit
function XGetWeaponDamageInfo(hit)
	local targetType;
	local targetComponent;
	local weaponClass;
	local weaponHitType;
	local weaponDamageOriginal = -1;
	local weaponDamageModded = nil;
	local weaponDamageMultiplier = 1;

	local target = hit.target;
	if (target and target.vehicle) then
		if (target.Properties and target.Properties.Modification) then
			targetType = target.class.."_"..target.Properties.Modification;
		else
			targetType = target.class;
		end
	else
		if (hit.targetId == hit.shooterId) then
			targetType = "Shooter";
		else
			targetType = "Player";
		end
	end

	if (hit.weapon and hit.weapon.class) then
		weaponClass = hit.weapon.class;
	else
		weaponClass = "unknown";
	end
	local weaponHitType = g_gameRules.game:GetHitType(hit.typeId);
	if (not weaponHitType) then
		if (hit.explosion) then
			weaponHitType = "explosion";
		else
			weaponHitType = "other";
		end
	end
	local weaponType = weaponClass.."_"..weaponHitType;

	local isHackOnLookup1 = false;
	weaponDamageOriginal, isHackOnLookup1 = XLookupOriginalWeaponDamage(weaponType, hit);

	local info = XVehicleInfo[targetType];
	if (info and not info.PartsById) then
		info = nil;
	end
	local partName = nil;
	local damageKey = nil;
	if (info) then
		if (hit.partId) then
			partName = info.PartsById[hit.partId];
		end
		if (not partName) then
			local localPos = target:ToLocal(0, hit.pos);
			for i, bounds in ipairs(info.PartsByBounds) do
				if (	(bounds.minBound.x < localPos.x) and
						(bounds.minBound.y < localPos.y) and
						(bounds.minBound.z < localPos.z) and
						(localPos.x < bounds.maxBound.x) and
						(localPos.y < bounds.maxBound.y) and
						(localPos.z < bounds.maxBound.z)) then
					partName = bounds.part;
					break;
				end
			end
		end
		if (not partName) then
			partName = info.DefaultPart;
		end
		damageKey = info.Parts[partName].damageKey;
		targetComponent = partName;
	end

	local isHackOnLookup2 = false;
	weaponDamageModded, isHackOnLookup2 = XLookupModdedWeaponDamage(weaponType, targetType, damageKey);

	weaponDamageMultiplier = weaponDamageModded/weaponDamageOriginal;
	return targetType, targetComponent, weaponClass, weaponHitType, weaponDamageOriginal,
		weaponDamageModded, weaponDamageMultiplier, (isHackOnLookup1 or isHackOnLookup2);
end

-- dumps the full data of a hit
function XHitDump(hit, msg)
	if (not XValve:forKey(XHitDump):heat()) then
		return;
	end
	LogHit("%s", msg or " ==== HIT DUMP");
	local hitClone = XShallowCloneTable(hit);
	hitClone.shooter = XEntityToString(hit.shooter);
	hitClone.target = XEntityToString(hit.target);
	hitClone.weapon = XEntityToString(hit.weapon);
	hitClone.x_impact_targetId = XEntityToString(System.GetEntity(hit.impact_targetId or 0));
	hitClone.x_projectileId = XEntityToString(System.GetEntity(hit.projectileId or 0));
	hitClone.x_shooterId = XEntityToString(System.GetEntity(hit.shooterId or 0));
	hitClone.x_targetId = XEntityToString(System.GetEntity(hit.targetId or 0));
	hitClone.x_weaponId = XEntityToString(System.GetEntity(hit.weaponId or 0));
	XTableDumper:Reset(XVar.hitLogFile);
	XTableDumper:Dump("hit", hitClone);
	XTableDumper:Reset();
	LogHit("%s", " ==== ");
end

-- ===============================================
-- Helpers to implement modifications
-- ===============================================

function XQueueCall(...)
	table.insert(XVar.onUpdateQueue, {...});
end

-- returns true if we have a server side game available
function XIsServerSideGameReady()
	if (not g_gameRules) then
		return false;
	end
	if (not g_gameRules.game) then
		return false;
	end
	if (g_gameRules.game:IsClient()) then
		return false;
	end
	return true;
end

function XEquipPlayer(actor, equipment)
	local primary = nil;
	for i, class in ipairs(equipment) do
		local itemId = ItemSystem.GiveItem(class, actor.id, false);
		if (XCheckAvailableWeaponAttachments(actor, class, itemId)) then
			primary = class;
		end
	end
	actor.actor:SelectItemByNameRemote(primary);
end

function XIncSpawnCounter()
	if (not XVar.spawnCounter) then
		XVar.spawnCounter = 0;
	end;
	XVar.spawnCounter = XVar.spawnCounter + 1;
	return XVar.spawnCounter;
end

-- add an entity and possibly schedule it for respawn
function XAddEntity(params)
	if (not params.name) then
		params.name = (params.class or "NIL").."_xspawned_"..tostring(XIncSpawnCounter());
	end
	local spawned = System.SpawnEntity(params);
	if (spawned) then
--		LogAlways("Succesfully spawned %s", spawned:GetName());
		LogAdmins("[AEGIS] Succesfully spawned %s as %s %s", spawned:GetName() or "N/A", spawned.class or "N/A", spawned.Properties.Modification or "N/A");

		if (spawned.vehicle) then
			if (params.x_teamId ~= nil) then
				g_gameRules.game:SetTeam(params.x_teamId, spawned.id);
			end
			spawned:AwakePhysics(1);
		end
	else
		LogAdmins("[AEGIS] SPAWN OF %s FAILED", params.class or params.archetype or "N/A");
	end
	return spawned;
end

function XAddWeapon(class, setup, pos, rotation, respawn)
	if (not rotation) then
		rotation = {x = 0.0, y = 0.0, z = 0.0};
	end
	if (not respawn or not tonumber(respawn)) then
		respawn = 60;
	end
	XAddEntity({
		class = class;
		position = pos;
		rotation = rotation;
		properties = {
			HitPoints = 0;
			initialSetup = setup;
			bMounted = 0;
			bPhysics = 0;
			bPickable = 1;
			soclasses_SmartObjectClass = "";
			bUsable = 0;
			Respawn = {
				bRespawn = 1;
				nTimer = respawn;
				bUnique = 0;
			};
		};
	});
end

-- Civ_car1 paints:  black, blue, green, police, red, silver, utility;
function XAddVehicle(name, class, mod, paint, pos, rotate, abandon, respawn)
	local teamId = nil;
	local teamName = nil;
	if (paint == "nk") then
		teamId = 1;
		teamName = "tan";
	end
	if (paint == "us") then
		teamId = 2;
		teamName = "black";
	end
	local bAbandon = 0;
	if (not abandon) then
		bAbandon = 1;
		abandon = 40;
	elseif (abandon>0) then
		bAbandon = 1;
	else
		abandon = nil;
	end
	local bRespawn = 0;
	if (not respawn) then
		bRespawn = 1;
		respawn = 10;
	elseif (respawn>0) then
		bRespawn = 1;
	else
		respawn = nil;
	end

	return XAddEntity({
		name = name;
		class = class;
		properties = {
			Modification = mod;
			Paint = paint,
			teamName = teamName;
			Respawn = {
				bAbandon = bAbandon;
				bRespawn = bRespawn;
				bUnique = 1;
				nAbandonTimer = abandon;
				nTimer = respawn;
			};
		};
		position = pos;
		orientation = rotate;
		x_teamId = teamId;
	});
end

-- relocate the entity with the specified id
function XRelocateEntity(id, pos, angles)
	local e = System.GetEntity(id);
	if (e) then
		local name = e:GetName();
		e:SetWorldPos(pos);
		e:SetWorldAngles(angles);
		LogInfo("[AEGIS] relocated entity #%d %s", id, name);
	end
end

-- remove the entity with the specified id
function XRemoveEntity(id)
	local e = System.GetEntity(id);
	if (e) then
		local name = e:GetName();
		System.RemoveEntity(e.id);
		LogInfo("[AEGIS] removed entity #%d %s", id, name);
	end
end

-- remove the first entity with the specified name
function XRemoveEntityByName(name)
	local e = System.GetEntityByName(name);
	if (e) then
		local class = e.class;
		System.RemoveEntity(e.id);
		LogInfo("[AEGIS] removed entity %s of class %s", name, class);
	end
end

-- remove all entities of the specified class
function XRemoveEntitiesByClass(class)
	local ents = System.GetEntitiesByClass(class);
	if (ents) then
		for i, e in ipairs(ents) do
			local name = e:GetName();
			System.RemoveEntity(e.id);
			LogInfo("[AEGIS] removed entity %s of class %s", name, class);
		end
	end
end

-- modify initial setup
function XModifyInitialSetup(class, modifications)
	local ents = System.GetEntitiesByClass(class);
	if (ents) then
		for i, e in ipairs(ents) do
			local name = e:GetName();
			local before = e.Properties.initialSetup;
			local setup = string.gsub(", "..before..", ", " ", "");
			for f, t in pairs(modifications) do
				if (t == "++") then
					if (not string.find(setup, ", "..f..", ")) then
						setup = setup..f..", ";
					end
				elseif (t == "--") then
					setup = string.gsub(setup, ", "..f..", ", ", ");
				else
					setup = string.gsub(setup, ", "..f..", ", ", "..t..", ");
				end
			end
			setup = string.gsub(setup, ", ,*", ", ");
			setup = string.gsub(setup, "^, ", "");
			setup = string.gsub(setup, ", $", "");
			local after = setup;
			if (before ~= after) then
				-- only modify if entity can respawn
				if (e.Properties and e.Properties.Respawn and e.Properties.Respawn.bRespawn and e.Properties.Respawn.bRespawn ~= 0) then
					e.Properties.initialSetup = after;
					g_gameRules.game:ScheduleEntityRespawn(e.id, false, 1);
					g_gameRules.game:ScheduleEntityRemoval(e.id, 3, false);
					LogInfo("[AEGIS] changed initial setup of entity %s of class %s from %s to %s", name, class, before, after);
				end
			end
		end
	end
end

-- remove all ammo pickups with specified ammo
function XRemoveAmmoPickup(ammo)
	local ents = System.GetEntitiesByClass("CustomAmmoPickup");
	if (ents) then
		local lammo = string.lower(ammo);
		for i, e in ipairs(ents) do
			-- XDumpObject("CAP", e);
			if (string.lower(e.Properties.AmmoName) == lammo) then
				local name = e:GetName();
				System.RemoveEntity(e.id);
				LogInfo("[AEGIS] removed pickup %s for ammo %s", name, ammo);
			end
		end
	end
end


----------------------------------------------------------------------------------
function XCheckAvailableWeaponAttachments(player, weaponClass, itemId)
	local attachmentClasses = XCfgVar.weaponAttachmentsInfo[weaponClass];
	if (not attachmentClasses) then
		return false;
	end
	if (not itemId) then
		itemId = player.inventory:GetItemByClass(weaponClass);
	end
	if (not itemId) then
		return false;
	end
	local item = System.GetEntity(itemId);
	if (not item) then
		return false;
	end
	for i, attachmentClass in ipairs(attachmentClasses) do
		local att = player.inventory:GetItemByClass(attachmentClass);
		if (att) then
			local currWeapon = item.weapon;
			if (currWeapon and currWeapon:SupportsAccessory(attachmentClass)) then
				currWeapon:AttachAccessory(attachmentClass, true, true);	-- force attach
			end
		end
	end
--	-- select weapon into hand
--	player.inventory:SetCurrentItemId(itemId);
	return true;
end


function XDumpInitialSetup(class)
	local ents = System.GetEntitiesByClass(class);
	if (ents) then
		for i, e in ipairs(ents) do
			local name = e:GetName();
			XDumpObject(name, e.Properties);
		end
	end
end

-- use with name no longer supported, caller should use XGetPlayerForName
-- and handle cases with multiple or no match appropriately.
function XCalcSpawnPlayerPos(player, distance)
	if (not player or type(player) == "string") then
		return nil, nil;
	end
	if (not distance) then
		distance = 1.0;
	end
	local pos = player:GetBonePos("Bip01 head");
--	local velo = player:GetBoneVelocity(0, "Bip01 head");
	local dir = player:GetBoneDir("Bip01 head");
--	local dir = player.actor:GetHeadDir();
	ScaleVectorInPlace(dir, tonumber(distance));
	FastSumVectors(pos, pos, dir);
	dir = player:GetDirectionVector(1);
	return pos, dir;
end


function XGetSSMFileForName(name)
	local dir = XRuntimeVar.serverRootDir;
	if (not dir) then
		dir = "";
	end
	return dir.."SSM/"..name;
end;

function XGetFlagFileForName(name)
	local sane = string.gsub(name, "[^%w_]", "");
	local dir = XRuntimeVar.serverFlagDir;
	if (not dir or (dir == "")) then
		dir = XRuntimeVar.serverRootDir;
	end
	if (not dir) then
		dir = "";
	end
	return dir..sane..".flag";
end;

-- ===============================================
-- Actual Modification Implementation
-- ===============================================

function XIsMapPopulated()
	local numPlayers = XGetPlayersCount();
--	local hasPlayer = (numPlayers>0);
--	local hasPlayers = (numPlayers>1);
--	local isInGame = g_gameRules:GetState() == "InGame";
--	return (hasPlayers or (isInGame and hasPlayer));
	return (numPlayers > 0);
end

function XResurrectTurrets()
	if (g_gameRules.game:GetRemainingGameTime()<XCfgVar.resurrectTurretMinTime) then
		return;
	end
	local tt = System.GetEntitiesByClass("AutoTurret");
	if (tt) then
		for i, t in ipairs(tt) do
			XResurrectTurret(t);
		end
	end
	tt = System.GetEntitiesByClass("AutoTurretAA");
	if (tt) then
		for i, t in ipairs(tt) do
			XResurrectTurret(t);
		end
	end
end


function XResurrectTurret(turret)
	local hp = turret.Properties.HitPoints;
	if (hp and hp>0) then
		if (not turret.x_repair_rate) then
			turret.x_repair_rate = XCfgVar.repairDamagedTurretRate;
		end
		local h1 = turret.item:GetHealth();
		if (h1 == 0) then
			if (not turret.x_resurrection_begin) then
				-- destruction detected
				turret.x_resurrection_begin = _time + XCfgVar.resurrectTurretDelay;
				turret.x_repair_rate = XCfgVar.repairResurrectedTurretRate;
				LogAdmins("[AEGIS] Completely destroyed turret #%q %q", tostring(turret.id), turret:GetName());
			elseif (_time > turret.x_resurrection_begin) then
				LogAdmins("[AEGIS] Resurrecting turret #%q %q", tostring(turret.id), turret:GetName());
				XRepairTurret(turret, turret.x_repair_rate);
				turret.x_resurrection_begin = nil;
				local h2 = turret.item:GetHealth();
				if (h2 >= hp) then
					-- fully repaired
					LogAdmins("[AEGIS] Completely repaired turret #%q %q", tostring(turret.id), turret:GetName());
					turret.x_repair_rate = XCfgVar.repairDamagedTurretRate;
				end
			end
		elseif (h1<hp) then
			XRepairTurret(turret, turret.x_repair_rate);
			local h2 = turret.item:GetHealth();
			if (h2 >= hp) then
				-- fully repaired
				LogAdmins("[AEGIS] Completely repaired turret #%q %q", tostring(turret.id), turret:GetName());
				turret.x_repair_rate = XCfgVar.repairDamagedTurretRate;
			end
		end
	end
end

function XRepairTurret(turret, amount)
	local hit = {
		damage = amount;
		dir = { x = 0.0, y = 0.0, z = 1.0 };
		materialId = 0;
		partId = -1;
		pos = turret:GetWorldPos();
		radius = 0;
		shooter = NULL_ENTITY;
		target = turret;
		type = "repair";
		typeId = g_gameRules.game:GetHitTypeId("repair");
		weapon = NULL_ENTITY;
	};
	turret.item:OnHit(hit);
end

--------------------------------------------------------------------------------
-- 1.3
--------------------------------------------------------------------------------
XSavedMessages = {};
function XSaveMessageForPlayer(profileid, ...)
    local id = tonumber(profileid);
    if (not id) then
	return;
    end

    local msg = table.concat({...}, " ");
    local player = XGetPlayerForIdentityValue("profile", profileid);
    if (not player) then
	--XSavedMessages.profileid.messages = nil;
	table.insert(XSavedMessages.profileid.messages, msg);
    else
	XSendChatToTarget("Admin: ",player,msg);
    end
end

System.AddCCommand("x_savemessage",		"XQueueCall(XSaveMessageForPlayer, %%)",			"x_savemessage [playername] [msg]");

-- ===============================================
-- Server Side Commands
-- ===============================================

System.AddCCommand("x_admin_add",		"XQueueCall(XAdminAdd, %1)",			"Adds a player name to the list of recognized admins.");
System.AddCCommand("x_admin_del",		"XQueueCall(XAdminRemove, %1)",			"Deletes a player name from the list of recognized admins.");
System.AddCCommand("x_admin_id_add",		"XQueueCall(XAdminIdAdd, %1)",			"Adds a profile id to the list of recognized admins.");
System.AddCCommand("x_admin_id_del",		"XQueueCall(XAdminIdRemove, %1)",		"Deletes a profile id from the list of recognized admins.");
System.AddCCommand("x_admin_list",		"XQueueCall(XAdminList)",			"Displays the list of recognized admins.");
System.AddCCommand("x_admin_spam",		"XQueueCall(XAdminSpamMessage, %%)",		"Spams an admin messsage in intervals to all players.");
System.AddCCommand("x_award_points",		"XQueueCall(XAwardPlayer, %%)",			"award a player with specified amounts of PP and CP (default: 10000/1000)");
System.AddCCommand("x_chat_tail",		"XQueueCall(XChatTail, %%)",			"x_chat_tail [n]  -- reprints the last n (default = 20) chat messages");
System.AddCCommand("x_delay",			"XQueueCall(XExecDelay, %%)",			"like x_exec but first arg is a delay in seconds");
System.AddCCommand("x_dump_name",		"XQueueCall(XDumpByName, %%)",			"dump the entity table of the named object");
System.AddCCommand("x_exec",			"XQueueCall(XExec, %%)",			"Execute a command in Lua, OS shell or game console");
System.AddCCommand("x_flag_reset",		"XQueueCall(XFlagReset, %1)",			"delete [arg#1].flag file");
System.AddCCommand("x_flag_set",		"XQueueCall(XFlagSet, %1)",			"create [arg#1].flag file");
System.AddCCommand("x_force_reload",		"XQueueCall(XReload, true)",			"reload configuration file X<mod>.lua and force it active immediately");
System.AddCCommand("x_force_map",		"XQueueCall(XForceNextMap, %1)",		"force the specified map to be used on the next map switch");
System.AddCCommand("x_get_chat_commands",	"XQueueCall(XGetChatCommands)",			"Reloads Chat Commands from directory.");
System.AddCCommand("x_hit_details",		"XQueueCall(XHitDetails, %%)",			"configure sending of hit details to administrators");
System.AddCCommand("x_hit_reports",		"XQueueCall(XHitReport, %%)",			"configure sending of hit reports to players");
System.AddCCommand("x_item_give",		"XQueueCall(XItemGive, %1, %2)",		"Gives the specified item to the player.");
System.AddCCommand("x_mkc_on",			"XQueueCall(XMKC, true)",			"enable mass kill countermeasure");
System.AddCCommand("x_mkc_off",			"XQueueCall(XMKC, false)",			"disable mass kill countermeasure");
System.AddCCommand("x_mute",			"XQueueCall(XMute, %1)",			"Mutes player");
System.AddCCommand("x_nextlevel",		"XQueueCall(XNextLevel)",			"switch to next map according to configured level rotation");
System.AddCCommand("x_next_level",		"XQueueCall(XNextLevel)",			"switch to next map according to configured level rotation");
System.AddCCommand("x_ping",			"XQueueCall(XPing)",				"print current stats for high ping feature");
System.AddCCommand("x_player_kill",		"XQueueCall(XKill, %1)",			"Kills a player");
System.AddCCommand("x_player_name",		"XQueueCall(XPlayerChangeName, %1, %2)",	"x_player_name [playername] [newname]");
System.AddCCommand("x_player_team",		"XQueueCall(XPlayerChangeTeam, %%)",		"x_player_team [playername] [team] or [Blank]");
System.AddCCommand("x_punish",			"XQueueCall(XPenaltyBox, %%)",			"puts a player in the 'penalty box' for specified number of minutes and sends him a msg");
System.AddCCommand("x_vehicle_boot",		"XQueueCall(XVehicleBoot, %1)",			"kicks player from a vehicle");
System.AddCCommand("x_unpunish",		"XQueueCall(XReleasePenaltyBox, %1)",		"Removes player from Penalty Box.");
System.AddCCommand("x_quit",			"XQueueCall(XQuit)",				"create quit.flag file and quit");
System.AddCCommand("x_reload",			"XQueueCall(XReload, false)",			"reload configuration file X<mod>.lua");
System.AddCCommand("x_remove_id",		"XQueueCall(XRemoveEntity, tonumber(%1))",	"x_remove_class [class]");
System.AddCCommand("x_remove_name",		"XQueueCall(XRemoveEntityByName, %1)",		"x_remove_class [class]");
System.AddCCommand("x_remove_class",		"XQueueCall(XRemoveEntitiesByClass, %1)",	"x_remove_class [class]");
System.AddCCommand("x_restart",			"XQueueCall(XRestart)",				"create restart.flag file and quit");
System.AddCCommand("x_say",			"XQueueCall(XAdminMessage, %%)",		"Sends an admin messsage to all players.");
System.AddCCommand("x_say_big",			"XQueueCall(XSendBigCenterMessage, %%)",	"Sends a big center messsage to all players");
System.AddCCommand("x_say_info",		"XQueueCall(XSendInfoMessage, %%)",		"Sends an info messsage to all players.");
System.AddCCommand("x_set_root_dir",		"XSetRootDir(%%)",				"Tell SSM the server's root directory");
System.AddCCommand("x_spawn",			"XQueueCall(XSpawn, %1, %2, %3)",		"x_spawn [playername] [distance] [class]");
System.AddCCommand("x_spawn_vehicle",		"XQueueCall(XSpawnVehicle, %%)",		"x_spawn_vehicle playername distance class [modification|-] [respawntime|-] [name]");
System.AddCCommand("x_teleport",		"XQueueCall(XTelePort, %%)",			"x_teleport [playername] [X] [Y] [Z]");
System.AddCCommand("x_time",			"XQueueCall(XTime, %1)",			"Sets the amount of time remaining in a round [minutes]");
System.AddCCommand("x_unmute",			"XQueueCall(XUnmute, %1)",			"UnMutes player");
System.AddCCommand("x_ver",			"XQueueCall(XVersion)",				"print version information of the mod");
System.AddCCommand("x_xport_config",		"XQueueCall(XExportConfig)",			"export currently effective and loaded configuration");
System.AddCCommand("x_xport_spawn",		"XQueueCall(XExportSpawnInfoByName, %%)",	"export spawn info for the named entity");
System.AddCCommand("x_xport_vehicle_defaults",	"XQueueCall(XExportVehicleDefaults)",		"export vehicle damage defaults extracted from XMLs");
System.AddCCommand("x_kick",			"XQueueCall(XKick,%%)",				"Kicks specified player");
System.AddCCommand("x_ban",			"XQueueCall(XBan,%%)",				"Bans specified player");
System.AddCCommand("x_profileids",		"XQueueCall(XProfileIDS)",			"Returns the profile ID of all connected players");
System.AddCCommand("x_players", 		"XQueueCall(XPlayers)", 			"Returns information about all connected players");

-- 1.4 Premium Commands
System.AddCCommand("x_premium_add",		"XQueueCall(XPremiumAdd, %1)",			"Adds a player name to the list of recognized premium players.");
System.AddCCommand("x_premium_del",		"XQueueCall(XPremiumRemove, %1)",		"Deletes a player name from the list of recognized premium players.");
System.AddCCommand("x_premium_id_add",		"XQueueCall(XPremiumIdAdd, %1)",		"Adds a profile id to the list of recognized premium players.");
System.AddCCommand("x_premium_id_del",		"XQueueCall(XPremiumIdRemove, %1)",		"Deletes a profile id from the list of recognized premium players.");
System.AddCCommand("x_premium_list",		"XQueueCall(XPremiumList)",			"Displays the list of recognized premium players.");

-- ===============================================

function XProfileIDS()
	local players = g_gameRules.game:GetPlayers();
	if (players) then
		for i, player in ipairs(players) do
			if (player and player.info and player.info.profile and (player.info.profile ~= "0")) then
				LogAdmins("[AEGIS] $3Name: $1%s $3ProfileId: $1%s", player:GetName(), player.info.profile)
			end
		end
	end
end

function XPlayers()
	local players=g_gameRules.game:GetPlayers();
	if (players) then
		LogAdmins("-------ip------ -port ------host----- chan --profile- -os stat pwr --------name-------- slot pbUpdated");
		for i,player in pairs(players) do
			if (player.info.buildInfo==0) then
				LogAdmins("%15s %5s %15s %3s %11s %3s %3s %3s %21s %3s %8s", player.info.ip, player.info.port, player.info.host, player.info.channel, player.info.profile, player.info.os, player.info.status, player.info.power, player.info.name, player.info.slot, player.info.pbUpdated);
				LogAdmins(    "--> PB GUID: %s", player.info.guid)
			end
		end
	end
end

-- fix PB rcon's say bug:
System.AddCCommand("say", "XFixPBSay(%%)", "say a message to all admins");
function XFixPBSay(...)
	local msg = table.concat({...}, " ");
	LogAdmins("%s", msg);
end

-- add easy 'go sth' command:
System.AddCCommand("go", "XGoCommand(%%)", "'go <name>' executes the command 'exec go_<name>.cfg'");
function XGoCommand(...)
	local tmp = {};
	for i, v in ipairs({...}) do
		table.insert(tmp, string.lower(string.gsub(v, "%W", "")));
	end
	local target = table.concat(tmp, "_");
	LogAdmins("[AEGIS] GO to target: %s", target);
	System.ExecuteCommand("exec go_"..target..".cfg");
end

--=============================================================================
-- Function: XKick
--
-- Used to kick players through Punkbuster
-- Aegis 1.2 update - Determines if PB is running, if not, uses non-pb kicks
--
-- \param[in] name
-- Name of player to be kicked, Duration of the kick
--
-- \param[in] timep
-- Length of the kick
--=============================================================================
function XKick(name, time, ...)
	local reason = table.concat({...}, " ");
	local player = XSimplePlayerForName(name, "x_kick");
	if (not player) then
		return;
	end
	if (not time) then
		LogAdmins("[AEGIS] X_Kick: Default time value used (5)");
		time = 5;
	end
	if (not reason) then
		LogAdmins("[AEGIS] X_Kick: Default reason used");
		reason = "(ADMIN DECISION)";
	end

	if (XCfgVar.sendChatOnXKick) then
		XSendChatToAll(nil, "%s has been removed from server. (ADMIN DECISION)", player:GetName());
	end
	if (XVar.punkbusterDetected) then
		System.ExecuteCommand(XFormat("pb_sv_kick %s %s %s", player:GetName(), time, reason));
	else
		local bto = System.GetCVar("ban_timeout");
		System.SetCVar("ban_timeout", time);
		CryAction.BanPlayer(player.id, XFormat("%s",reason)); --Crytek ban
		System.SetCVar("ban_timeout", bto);
	end
end


--=============================================================================
-- Function: XBan
--
-- Used to ban players through Punkbuster
-- Aegis 1.2 update - Determines if PB is running, if not, uses non-pb bans
--
-- \param[in] name
-- Name of player to be banned
--=============================================================================
function XBan(name, ...)
	local reason = table.concat({...}, " ");
	local player = XSimplePlayerForName(name, "x_ban");
	if (not player) then
		return;
	end
	if (not reason) then
		LogAdmins("[AEGIS] X_Ban: Default reason used");
		reason = "(ADMIN DECISION)";
	end
	if (XCfgVar.sendChatOnXKick) then
		XSendChatToAll(nil, "%s has been banned.", player:GetName());
	end
	XPermaBan(player, reason);
	if (XVar.punkbusterDetected) then
		System.ExecuteCommand(XFormat("pb_sv_ban %s %s", player:GetName(), reason));
	else
		local bto = System.GetCVar("ban_timeout");
		System.SetCVar("ban_timeout", 1440);
		CryAction.BanPlayer(player.id, "ADMIN DECISION"); --Crytek ban
		System.SetCVar("ban_timeout", bto);
	end
end

-- ===============================================

function XVehicleBoot(name)
	local player = XSimplePlayerForName(name, "x_vehicle_boot");
	if (not player) then
		return;
	end
	if (player:IsOnVehicle()) then
		local vehicle = System.GetEntity(player.actor:GetLinkedVehicleId());
		vehicle.vehicle:ExitVehicle(player.id);
		if (XCfgVar.sendChatOnXVehicleBoot) then
			XSendChatToAll(nil, "%s has forced out of his vehicle. (ADMIN DECISION)", player:GetName());
		end
	end
end

-- ===============================================
function XAdminList()
	for i,n in pairs(XCfgVar.adminIds) do
		local player = XGetPlayerForIdentityValue("profile", i);
		if (player) then
			player = XFormat("$3%s",player:GetName());
		else
			player = "$4Offline";
		end
		LogAdmins("[AEGIS] Profile: %s Player: %s",i,player);
	end
	LogAdmins("$3[AEGIS] -- End of the list of recognized admins.");
end

-- ===============================================
function XAdminIdAdd(id)
	if (XCfgVar.adminIds[id]) then
		LogAdmins("$6[AEGIS] id %s is already in the list of recognized admins.",id);
		return;
	end
	XCfgVar.adminIds[id]=true;
	LogAdmins("$3[AEGIS] Added id %s to the list of recognized admins.",id);
end

-- ===============================================
function XAdminIdRemove(id)
	if (XCfgVar.adminIds[id]) then
		XCfgVar.adminIds[id] = nil;
		LogAdmins("$3[AEGIS] Deleted id %s from the list of recognized admins.",id);
		return;
	else
	LogAdmins("$4[AEGIS] id %s is not on the list of recognized admins.",id);
	end
end

-- ===============================================

function XAdminAdd(name)
	local player = XSimplePlayerForName(name,"x_admin_add");
	if (not player) then
		return;
	end
	local profile = XGetIdentityValue(player, "profile");
	if (profile=="0") then
		LogAdmins("$4[AEGIS] profile id of %q is unknown.",player:GetName());
		return;
	end
	if (XCfgVar.adminIds[profile]) then
		LogAdmins("$6[AEGIS] %q is already in the list of recognized admins.",player:GetName());
		return;
	end
	XCfgVar.adminIds[profile] = true;
	LogAdmins("$3[AEGIS] Added %q with id %s to the list of recognized admins.",player:GetName(),profile);
end

-- ===============================================
function XAdminRemove(name)
	local player = XSimplePlayerForName(name,"x_admin_del");
	if (not player) then
		return;
	end
	local profile = XGetIdentityValue(player, "profile");
	if (profile=="0") then
		LogAdmins("$4[AEGIS] profile id of %q is unknown.",player:GetName());
		return
	end
	if (XCfgVar.adminIds[profile]) then
		XCfgVar.adminIds[profile] = nil;
		LogAdmins("$3[AEGIS] Deleted %q with id %s from the list of recognized admins.",player:GetName(),profile);
		return;
	end
	LogAdmins("$6[AEGIS] %q is not on the list of recognized admins.",player:GetName());
end

-- ===============================================

function XAdminMessage(...)
	local msg = table.concat({...}, " ");
	LogAdmins("[AEGIS] ADMIN SAYS: %s", msg);
	if (XCfgVar.sendChatOnXSay) then
		XSendChatToAll(nil, "%s", msg);
	end
	XSendServerMessageToAllRemote("%s", msg);
	table.insert(XVar.adminMessages, msg);
	if (#XVar.adminMessages == 1) then
		XVar.adminMessageRepeatCount = XCfgVar.adminMessageDuration;
	end
end

-- ===============================================

function XAdminSpamMessage(...)
	local msg = table.concat({...}, " ");
	if (not XCfgVar.useAdminSpamFeature) then
		LogAdmins("[AEGIS] Admin spam feature is disabled.");
	else
		if (msg == "-") then
			XVar.adminSpamMsg = nil;
			LogAdmins("[AEGIS] Stopped admin spam.");
		elseif (msg == "") then
			XVar.adminSpamMsg = XCfgVar.adminSpamDefaultMsg;
			LogAdmins("[AEGIS] New admin spam: %s", XVar.adminSpamMsg);
		else
			XVar.adminSpamMsg = msg;
			LogAdmins("[AEGIS] New admin spam: %s", XVar.adminSpamMsg);
		end
		XVar.adminSpamRepeatCount = nil;
	end
end

-- ===============================================

function XAwardPlayer(name, amountPP, amountCP)
	--
	-- This feature requires that the AwardPPCount and AwardCPCount functions
	-- be defined. They are defined in the PowerStruggle mode files by default.
	--
	if (g_gameRules.AwardPPCount and g_gameRules.AwardCPCount) then
		--
		-- Get valid PP and CP values to give to the specified players. This is
		-- either the amount passed into the function or the spefified default
		-- value if the ones passed in are invalid.
		--
		local nPP = tonumber(amountPP) or 0;
		local nCP = tonumber(amountCP) or 0;

		--
		-- To make this function generic to a single player, all players, or
		-- only players on a specific team, we have to set a variable to use
		-- later on. That variable is teamId. Note that if teamId == nil then
		-- we are awarding the points to a single player whose name is specified
		-- in the name variable passed into this function.
		--
		local teamId = 0;
		if ( string.lower(tostring(name)) == "all" ) then
			teamId = 3;
		elseif ( string.lower(tostring(name)) == "nk" ) then
			teamId = 1;
		elseif ( string.lower(tostring(name)) == "us" ) then
			teamId = 2;
		end

		--
		-- Check for the single player award case.
		--
		if ( teamId == 0 ) then
			local player = XSimplePlayerForName(name, "x_award_points");

			if (not player) then
				return;
			end

			g_gameRules:AwardPPCount(player.id, nPP);
			g_gameRules:AwardCPCount(player.id, nCP);

			if (XCfgVar.sendChatOnXAward) then
				XSendChatToAll(nil, "%q was awarded %d PP and %d CP (ADMIN DECISION)", player:GetName(), nPP, nCP);
			end

			LogAdmins("[AEGIS] awarded %d PP and %d CP to %q", nPP, nCP, player:GetName());
		else
			--
			-- We are awarding points to either all players or players on the
			-- specified team.
			--
			local players = System.GetEntitiesByClass("Player");

			for i, player in ipairs(players) do
				playerTeamId = g_gameRules.game:GetTeam(player.id);
				if ( teamId == 3 or playerTeamId == teamId ) then
					g_gameRules:AwardPPCount(player.id, nPP);
					g_gameRules:AwardCPCount(player.id, nCP);

					if (XCfgVar.sendChatOnXAward) then
						XSendChatToAll(nil, "%q was awarded %d PP and %d CP (ADMIN DECISION)", player:GetName(), nPP, nCP);
					end
				end
			end

			LogAdmins("[AEGIS] awarded %d PP and %d CP to "..string.upper(tostring(name)), nPP, nCP);
		end
	end
end

-- ===============================================

function XChatTail(tail, ...)
	if (not XVar.chatMessages) then
		XVar.chatMessages = {};
	end
	local n = tonumber(tail) or 20;
	if (#XVar.chatMessages == 0) then
		SendAdmins("[AEGIS] x_chat_tail: no chat messages to show");
		return
	end
	local last = #XVar.chatMessages;
	local first = math.min(math.max(1, last+1-n), last);
	for i = first, last, 1 do
		local chatMsg = XVar.chatMessages[i];
		if (SSM) then
			SendAdmins("$9[AEGIS] CHAT %s ago %s", XTimeAgo(_time-chatMsg.time), chatMsg.textColored);
		else
			if	(chatMsg.type == ChatToTarget) then
				SendAdmins("$9[AEGIS] CHAT %s ago $8%s$9 to $8%s$9: $5%s", XTimeAgo(_time-chatMsg.time), chatMsg.sender, chatMsg.target, chatMsg.text);
			else
				SendAdmins("$9[AEGIS] CHAT %s ago $8%s$9 to $1%s$9: $5%s", XTimeAgo(_time-chatMsg.time), chatMsg.sender, chatMsg.target, chatMsg.text);
			end
		end
	end
end

-- ===============================================

function XDumpByName(...)
	local name = table.concat({...}, " ");
	local entity = System.GetEntityByName(name);
	if (not entity) then
		LogAdmins("[AEGIS] x_dump_name: no entity named '%s' found", name);
		return;
	end
	local fname = XGetSSMFileForName(entity:GetName()..".txt");
	local file, err = io.open(fname, "w+");
	if (not file) then
		LogAlways("[AEGIS] FILE ERROR: %s", err);
		XDumpObject(name, entity, 3, true, true);
	else
		XTableDumper:Reset(file);
		file:write("-- DUMP of "..name.."\n");
		file:write("\n");
		XDumpObjectToFile(file, name, entity, 20, true, true);
		file:close();
	end
end

-- ===============================================

function XExec(...)
	local cmd = table.concat({...}, " ");
	LogAdmins("[AEGIS] x_exec %s", cmd);
	local fc = string.sub(cmd, 1, 1);
	if (fc == "!") then
		local status = XSafeCall(os.execute, string.sub(cmd, 2));
		if (status) then
			LogAdmins("[AEGIS] x_exec %s returned: %s", cmd, tostring(status));
		else
			LogAdmins("[AEGIS] x_exec %s FAILED UNEXPECTEDLY", cmd);
		end
	elseif (fc == "#") then
		local chunk, error = loadstring(string.sub(cmd, 2), cmd);
		if (chunk) then
			XDebugCall(chunk);
		else
			LogAdmins("[AEGIS] x_exec %s failed: %s", cmd, tostring(error));
		end
	else
		System.ExecuteCommand(cmd);
	end
end

-- ===============================================

function XExecDelay(delay, ...)
	local dt = tonumber(delay);
	local cmd = table.concat({...}, " ");
	LogAdmins("[AEGIS] x_delay %s %s", delay, cmd);
	if (not dt) then
		LogAdmins("[AEGIS] x_delay: '%s' is not a valid delay", delay);
		return;
	end
	Script.SetTimer(dt*1000, function() XQueueCall(XExec, cmd); end);
end

-- ===============================================

function XExportConfig()
	local file, err = io.open(XGetSSMFileForName("SSM_xport_config.lua"), "w+");
	if (not file) then
		LogAlways("[AEGIS] FILE ERROR: %s", err);
	else
		XTableDumper:Reset(file);
		file:write("-- EXPORTED SSM CONFIGURATION\n");
		file:write(XFormat("-- Current GameRules: %s\n", XVar.currentMapInfo.mapRules));
		file:write(XFormat("-- Current Map Name : %s\n", XVar.currentMapInfo.mapName));
		file:write("\n");
		XTableDumper:Dump("XCurrentConfig", XCfgVar);
		file:write("\n");
		XTableDumper:Dump("XConfig", XConfig);
		file:write("\n");
		XTableDumper:Dump("XSetup", XSetup);
		file:write("\n");
		file:write("\n");
		file:close();
	end
end

-- ===============================================

function XExportSpawnInfoByName(...)
	local name = table.concat({...}, " ");
	local entity = System.GetEntityByName(name);
	if (not entity) then
		LogAdmins("[AEGIS] x_export_spawn: no entity named '%s' found", name);
		return;
	end

	local ep = {
		class = entity.class;
		name = entity:GetName();
		properties = {
			Respawn = {
				bRespawn = 1;
				nTimer = 30;
				bUnique = 1;
				bAbandon = 1;
				nAbandonTimer = 45;
			};
		};
	};
	if (entity.Properties and entity.Properties.Modification) then
		ep.properties.Modification = entity.Properties.Modification;
	end
	XExportSpawnInfo(entity, ep);
end

-- ===============================================

function XExportVehicleStripParts(part)
	part.Parts = XArrayToTable(part.Parts, "name", XExportVehicleStripParts);
	for k, v in pairs(part) do
		if ((k ~= "name") and (k ~= "Parts") and (k ~= "component")) then
			part[k] = nil;
		end
	end
	return part;
end

function XExportVehicleDefaults()
	local defaults = {};
	for i, vehicle in ipairs(VehicleSystem.VehicleImpls) do
		if ((vehicle ~= "Alien_warrior") and (vehicle ~= "DefaultVehicle") and (vehicle ~= "US_transportVTOL")) then
			local d = VehicleSystem.LoadXML(vehicle);
			defaults[vehicle] = d;
			d.Actions = nil;
			d.Animations = nil;
			local partCount = 0
			d.Components = XArrayToTable(d.Components, "name",
				function (component)
					component.DamageBehaviors = nil;
					component.name = nil;
					component.hasBounds = not component.useBoundsFromParts;
					component.useBoundsFromParts = nil;
					if (component.maxBound.x < component.minBound.x) then
						local swap = component.maxBound.x;
						component.maxBound.x = component.minBound.x;
						component.minBound.x = swap;
					end
					if (component.maxBound.y < component.minBound.y) then
						local swap = component.maxBound.y;
						component.maxBound.y = component.minBound.y;
						component.minBound.y = swap;
					end
					if (component.maxBound.z < component.minBound.z) then
						local swap = component.maxBound.z;
						component.maxBound.z = component.minBound.z;
						component.minBound.z = swap;
					end
					component.volume =
							(component.maxBound.x-component.minBound.x)
						*	(component.maxBound.y-component.minBound.y)
						*	(component.maxBound.z-component.minBound.z);
					component.DamageSplashMultipliers = XArrayToTable(component.DamageMultipliers, "damageType",
						function (tmp)
							return tmp.splash;
						end);
					component.DamageMultipliers = XArrayToTable(component.DamageMultipliers, "damageType",
						function (tmp)
							return tmp.multiplier;
						end);
					return component;
				end);
			if (d.Damages) then
				local dmg = d.Damages;
				dmg.DamageBehaviors = nil;
				dmg.DamageGroups = nil;
				dmg.groundCollisionMaxMult = nil;
				dmg.groundCollisionMaxSpeed = nil;
				dmg.groundCollisionMinMult = nil;
				dmg.groundCollisionMinSpeed = nil;
				dmg.submergedDamageMult = nil;
				dmg.submergedRatioMax = nil;

				dmg.DamageSplashMultipliers = XArrayToTable(dmg.DamageMultipliers, "damageType",
					function (tmp)
						return tmp.splash;
					end);
				dmg.DamageMultipliers = XArrayToTable(dmg.DamageMultipliers, "damageType",
					function (tmp)
						return tmp.multiplier;
					end);
			end;
			d.Inventory = nil;
			d.Materials = nil;
			d.Modifications = XArrayToTable(d.Modifications, nil,
				function (mod)
					return mod.name;
				end);
			d.MovementParams = nil;
			d.Paints = nil;
			d.Particles = nil;
			d.Parts = nil; -- XArrayToTable(d.Parts, "name", XExportVehicleStripParts);
			d.Physics = nil;
			d.Seats = nil;
			d.SeatGroups = nil;
			d.SpecialLocations = nil;
			d.WheelMaster = nil;
			d.VeedDefaults = nil;
			d.actionMap = nil;
		end
	end
	local file, err = io.open(XGetSSMFileForName("ServerModVehicleDefaults.lua"), "w+");
	if (not file) then
		LogAlways("[AEGIS] FILE ERROR: %s", err);
	else
		XTableDumper:Reset(file);
		XTableDumper:Dump("XVehicleDefaults", defaults);
		file:close();
	end
	return defaults;
end

-- ===============================================

function XFlagReset(name)
	os.remove(XGetFlagFileForName(name));
end

-- ===============================================

function XFlagSet(name)
	local file, err = io.open(XGetFlagFileForName(name), "w+");
	if (file) then
		file:write(name);
		file:close();
	end
end

-- ===============================================

function XForceNextMap(mapname)
	local verified = false;
	local mapsrch = "^.*/"..string.gsub(string.lower(mapname), "(%W)", "%%%1").."$";
	local mappath = string.lower(mapname);
	if (XVar.mapPaths) then
		for map, v in pairs(XVar.mapPaths) do
			if (map == string.lower(mapname)) then
				verified = true;
				mappath = map;
			elseif (string.match(map, mapsrch)) then
				verified = true;
				mappath = map;
			end
		end
	end
	if (verified) then
		LogAdmins("[AEGIS] Next map forced to: %s", mappath);
	else
		LogAdmins("[AEGIS] Next map forced to UNRECOGNIZABLE MAP: %s", mappath);
	end
	XCfgVar.mapForceAsNextLevel = mappath;
end

-- ===============================================

function XHitDetails(mode)
	local toggle = X_TOGGLE_ID[mode or X_TOGGLE_ID];
	if (toggle) then
		if (toggle == 1) then
			XCfgVar.sendHitDetailsToAdmins = true;
		elseif (toggle == 0) then
			XCfgVar.sendHitDetailsToAdmins = false;
		else
			XCfgVar.sendHitDetailsToAdmins = not XCfgVar.sendHitDetailsToAdmins;
		end
	end
	LogAdmins("[AEGIS] Sending of hit details to administrators is: %s", X_ONOFF_LABEL[XCfgVar.sendHitDetailsToAdmins]);
end

-- ===============================================

function XHitReport(mode)
	local toggle = X_TOGGLE_ID[mode or X_TOGGLE_ID];
	if (toggle) then
		if (toggle == 1) then
			XCfgVar.sendHitReportsToPlayers = true;
		elseif (toggle == 0) then
			XCfgVar.sendHitReportsToPlayers = false;
		else
			XCfgVar.sendHitReportsToPlayers = not XCfgVar.sendHitReportsToPlayers;
		end
	end
	LogAdmins("[AEGIS] Sending of hit reports to players is: %s", X_ONOFF_LABEL[XCfgVar.sendHitReportsToPlayers]);
end

-- ===============================================

function XItemGive(name, item)
	--
	-- To make this function generic to a single player, all players, or
	-- only players on a specific team, we have to set a variable to use
	-- later on. That variable is teamId. Note that if teamId == nil then
	-- we are giving the item to a single player whose name is specified
	-- in the name variable passed into this function.
	--
	local teamId = 0;
	if ( string.lower(tostring(name)) == "all" ) then
		teamId = 3;
	elseif ( string.lower(tostring(name)) == "nk" ) then
		teamId = 1;
	elseif ( string.lower(tostring(name)) == "us" ) then
		teamId = 2;
	end

	--
	-- Check for the single player give case.
	--
	if ( teamId == 0 ) then
		local player = XSimplePlayerForName(name, "x_item_give");
			if (not player) then
				return;
			end
		ItemSystem.GiveItem(item, player.id, false);
		local itemId = player.inventory:GetItemByClass(item);
		if (itemId) then
			LogAdmins("[AEGIS] %q was armed with %s", player:GetName(), item);
			if (XCfgVar.sendChatOnXItemGive) then
				XSendChatToAll(nil, "%q has been armed with a %s. (ADMIN DECISION)", player:GetName(), item);
			end
		else
			LogAdmins("[AEGIS] arming of %q with %s failed", player:GetName(), item);
		end
	else
		--
		-- We are giving the item to either all players or players on the
		-- specified team.
		--
		local players = System.GetEntitiesByClass("Player");

		for i, player in ipairs(players) do
			playerTeamId = g_gameRules.game:GetTeam(player.id);
			if ( teamId == 3 or playerTeamId == teamId ) then
				ItemSystem.GiveItem(item, player.id, false);
				local itemId = player.inventory:GetItemByClass(item);
				if (itemId) then
					LogAdmins("[AEGIS] %q was armed with %s", player:GetName(), item);
					if (XCfgVar.sendChatOnXItemGive) then
						XSendChatToAll(nil, "%q has been armed with a %s. (ADMIN DECISION)", player:GetName(), item);
					end
				else
					LogAdmins("[AEGIS] arming of %q with %s failed", player:GetName(), item);
				end
			end
		end
	end
end

-- ===============================================

function XMKC(enabled)
	if (enabled) then
		XRuntimeVar.isMassKillCountermeasureEnabled = true;
		LogAdmins("[AEGIS] Mass Kill Countermeasure enabled");
	else
		XRuntimeVar.isMassKillCountermeasureEnabled = false;
		LogAdmins("[AEGIS] Mass Kill Countermeasure disabled");
		XAdminMessage("Mass Kill Countermeasure disabled  -- Continue your game.");
	end
end

-- ===============================================

function XNextLevel()
	g_gameRules:XtendedNextLevel();
end

-- ===============================================

function XPenaltyBox(name, sDuration, ...)
	local msg = table.concat({...}, " ");
	local player = XSimplePlayerForName(name, "x_punish");
	local quit = false;
	local profile = XGetIdentityValue(player, "profile");
	if (not player or profile=="0") then
		quit = true;
	end
	local duration = tonumber(sDuration);
	if (not duration) then
		LogAdmins("[AEGIS] x_punish: you must specify a duration in minutes");
		quit = true;
	end
	if (msg == "") then
		LogAdmins("[AEGIS] x_punish: reason message missing");
		quit = true;
	end
	if (player.actor:GetSpectatorMode() ~= 0) then
		LogAdmins("[AEGIS] x_punish: you cannot punish players that are only spectating");
		quit = true;
	end
	if (AEGIS.PlayerInformationByProfile[profile] and AEGIS.PlayerInformationByProfile[profile].PenaltyBox) then
		LogAdmins("[AEGIS] x_punish: you cannot punish players that are already in the penalty box");
		quit = true;
	end
	if (quit) then
		return;
	end
	if (duration < 1) then
		LogAdmins("[AEGIS] x_punish: adjusted to minimal duration of one minute");
		duration = 1.0;
	end
	if (duration > 15) then
		LogAdmins("[AEGIS] x_punish: adjusted to maximum duration of fifteen minutes");
		duration = 15.0;
	end

	--
	-- If the player is the owner of a vehicle then make sure all the seats in
	-- the vehicle are unlocked.
	--
	local vehicleId = player.actor:GetLinkedVehicleId();
	if (vehicleId) then
		local vehicle = System.GetEntity(vehicleId);
		if (vehicle and vehicle.x_lock and vehicle.x_lock == player.id) then
			vehicle.x_lock = nil;
		end
	end

	if (AEGIS.PlayerInformationByProfile[profile]) then
		AEGIS.PlayerInformationByProfile[profile].PenaltyBox = {
			minutes = duration;
			message = msg;
			duration = 60*duration;
			startTime = _time;
		};
		LogAdmins("[AEGIS] punishing %q for %s", player:GetName(), msg);
		if (XCfgVar.sendChatOnXPunish) then
			XSendChatToAll(nil, "Player %q was penalized for %d minutes for %s (ADMIN DECISION)", player:GetName(), duration, msg);
		end
		g_gameRules:KillPlayer(player);
		g_gameRules:QueueRevive(player.id);
	end
end

-- ===============================================

function XReleasePenaltyBox(name, timeout)
	local player = XSimplePlayerForName(name, "x_unpunish");
	if (not player) then
		return;
	end

	local profile = XGetIdentityValue(player, "profile");
	if (profile=="0" or not AEGIS.PlayerInformationByProfile[profile]) then
		return;
	end

	if (not AEGIS.PlayerInformationByProfile[profile].PenaltyBox) then
		LogAdmins("[AEGIS] x_unpunish: Player already not in penalty box.");
		return;
	end

	AEGIS.PlayerInformationByProfile[profile].PenaltyBox = nil;
	ItemSystem.GiveItem("AlienCloak", player.id, false);
	ItemSystem.GiveItem("OffHand", player.id, false);
	ItemSystem.GiveItem("Fists", player.id, false);
	if (timeout) then
		if (XCfgVar.sendChatOnXUnpunish) then
			XSendChatToAll(nil, "%q was automatically removed from the penalty box (TIMEOUT)", player:GetName());
		end
	else
		if (XCfgVar.sendChatOnXUnpunish) then
			XSendChatToAll(nil, "%q was removed from the penalty box (ADMIN DECISION)", player:GetName());
		end
	end
end

-- ===============================================

function XPing()
	if (not XCfgVar.useHighPingFeature) then
		LogAdmins("[AEGIS] ping stats unavailable, high ping feature is disabled");
		return;
	end
	LogAdmins("[AEGIS] %8s %5s %5s %s", "current", "total", "above", "name");
	LogAdmins("[AEGIS] %8d %5d %5d to kick & %d to inform",
			XCfgVar.highPingTimeThreshold,
			XCfgVar.highPingTotalSamples,
			XCfgVar.highPingSamplesToKick,
			XCfgVar.highPingSamplesToInform);
	local players = g_gameRules.game:GetPlayers();
	if (players) then
		for i, player in ipairs(players) do
			if (player and player.actor:GetChannel()) then
				local ping = math.floor((g_gameRules.game:GetPing(player.actor:GetChannel()) or 0)*1000+0.5);
				LogAdmins("[AEGIS] %8d %5d %5d '%s'",
						player.x_pingSampleCurrent or -1,
						player.x_pingSamplesTotal or -1,
						player.x_pingSamplesAbove or -1,
						player:GetName());
			end
		end
	end
end

-- ===============================================

function XPlayerChangeTeam(playername, team)
	local player = XSimplePlayerForName(playername, "x_player_team");
	if (not player) then
		return;
	end

	local dstTeamId = 1;

	if ( not team ) then
		local oldTeamId = g_gameRules.game:GetTeam(player.id);
		if ( oldTeamId == 1 ) then
			dstTeamId = 2;
		end

		if (not dstTeamId) then
			LogAdmins("[AEGIS] x_player_team: Destination team could not be determined.");
			return;
		end
	else
		dstTeamId = X_TEAM_INDEX[string.lower(team)];

		if (not dstTeamId) then
			LogAdmins("[AEGIS] x_player_team: Invalid team, valid are 0, 1, 2, Spectator, nk, us");
			return;
		end
	end

	if ((g_gameRules.class ~= "PowerStruggle") and (g_gameRules.class ~= "TeamInstantAction")) then
		LogAdmins("[AEGIS] x_player_team: there are no teams with current game rules.");
		return;
	end

	local oldTeamId = g_gameRules.game:GetTeam(player.id);
	if (dstTeamId ~= oldTeamId) then
		if (XCfgVar.sendChatOnXPlayerTeam) then
			XSendChatToAll(nil, "%q was moved to the %s team (ADMIN DECISION)", player:GetName(), X_TEAM_LABEL[dstTeamId]);
			end
		if (player.actor:GetHealth()>0 and player.actor:GetSpectatorMode() == 0) then
			g_gameRules:KillPlayer(player);
		end
		g_gameRules.game:SetTeam(dstTeamId, player.id);
		if (dstTeamId ~= 0) then
			g_gameRules.Server.RequestSpawnGroup(g_gameRules, player.id, g_gameRules.game:GetTeamDefaultSpawnGroup(dstTeamId) or NULL_ENTITY, true);
			g_gameRules:QueueRevive(player.id);
		end
		LogAdmins("[AEGIS] %q was changed from team %s to team %s", player:GetName(), X_TEAM_LABEL[oldTeamId], X_TEAM_LABEL[dstTeamId]);
	else
		LogAdmins("[AEGIS] %q already on team %s", player:GetName(), X_TEAM_LABEL[dstTeamId]);
	end
end

-- ===============================================

function XPlayerChangeName(playername, newname)
	local player = XSimplePlayerForName(playername, "x_player_name");
	if (not player) then
		return;
	end
	local saneName, isFunnyName = XSanitizePlayerName(newname);
	if (newname ~= saneName) then
		LogAdmins("[AEGIS] x_player_name: provided new name %q would be sanitized to %q", newname, saneName);
		return;
	end
	local origName = player:GetName();
	g_gameRules.game:RenamePlayer(player.id, saneName);
	table.insert(player.x_names, {
		originalName = origName,
		changedName = saneName,
		cause = "order",
	});
	player.x_knownAs = saneName;
	if (isFunnyName) then
		player.x_funnyName = saneName;
	end
	LogAdmins("[AEGIS] Player %q renamed to %q by admin", origName, saneName);
	if (XCfgVar.sendChatOnRename) then
		XSendChatToAll(nil, "%q was renamed to %q (ADMIN DECISION)", origName, saneName);
	end
end

-- ===============================================

function XQuit()
	XFlagSet("quit");
	System.Quit();
end

-- ===============================================

function XRestart()
	XFlagSet("restart");
	System.Quit();
end


-- ===============================================

function XSetRootDir(...)
	local dir = table.concat({...}, " ");
	if ((dir == "") or (dir == "-")) then
		dir = nil;
	else
		local lc = string.sub(dir, -1);
		if (lc ~= "/") then
			dir = dir .. "/";
		end
	end
	XRuntimeVar.serverRootDir = dir or "";
	LogAdmins("[AEGIS] server's root directory now: %s", dir or "[RESET]");
end

-- ===============================================

function XSpawn(playername, distance, class)
	local player = XSimplePlayerForName(playername, "x_spawn");
	if (not player) then
		return;
	end
	local pos, dir = XCalcSpawnPlayerPos(player, distance);
	local ep = {
		class = class;
		position = pos;
		orientation = dir;
		name = class..tostring(XIncSpawnCounter());
	};
	local spawned = System.SpawnEntity(ep);
	if (spawned) then
		LogAdmins("[AEGIS] Succesfully spawned %s", spawned:GetName());
		if (XCfgVar.sendChatOnXSpawn) then
			XSendChatToAll(nil, "Admin Successfully Spawned %q (ADMIN DECISION)", spawned:GetName());
		end
	else
		LogAdmins("[AEGIS] SPAWN OF %s FAILED", class);
	end
	return spawned;
end

-- ===============================================

function XSpawnVehicle(playername, distance, class, mod, respawnTime, name)
	if ((not class) or (not distance) or (not playername)) then
		LogAdmins("[AEGIS] missing arguments");
		LogAdmins("Usage: x_spawn_vehicle playername distance class [modification|-] [respawntime|-] [name]");
		return;
	end
	if (mod == "-") then
		mod = nil;
	end
	local player = XSimplePlayerForName(playername, "x_spawn_vehicle");
	if (not player) then
		return;
	end
	local pos, dir = XCalcSpawnPlayerPos(player, distance);
	local params = {
		class = class;
		position = {x = pos.x, y = pos.y, z = pos.z+4.0};
		orientation = dir;
		name = name or class.."_"..(mod or "").."_"..tostring(XIncSpawnCounter());
		properties = {
			Modification = mod;
			Paint = name;
			Respawn = {
				bRespawn = 0;
				nTimer = tonumber(respawnTime) or 30;
				bUnique = 1;
				bAbandon = 0;
				nAbandonTimer = 0;
			};
		};
	};
	if (tonumber(respawnTime)) then
		params.properties.Respawn.bRespawn = 1;
	end

	local teamId = g_gameRules.game:GetTeam(player.id);
	params.x_teamId = teamId;
	if (teamId ~= 0 and g_gameRules.VehiclePaint) then
		params.properties.Paint = g_gameRules.VehiclePaint[g_gameRules.game:GetTeamName(teamId)] or "";
	end

	local spawned = System.SpawnEntity(params);

	if (spawned) then
		LogAdmins("[AEGIS] Succesfully spawned %s as %s %s", spawned:GetName() or "N/A", spawned.class or "N/A", spawned.Properties.modification or "N/A");
		g_gameRules.game:SetTeam(teamId, spawned.id);
		spawned:AwakePhysics(1);
		if (XCfgVar.sendChatOnXSpawn) then
			XSendChatToAll(nil, "Admin Successfully Spawned %q (ADMIN DECISION)", spawned:GetName() or "N/A", spawned.class or "N/A", spawned.Properties.modification or "N/A");
		end
		Script.SetTimer(15000, function() XExportSpawnInfo(spawned, params); end);
	else
		LogAlways("[AEGIS] SPAWN OF %s FAILED", class);
	end
	return spawned;
end

-- ===============================================

function XTelePort(playername, a, b, c)
	local player = XSimplePlayerForName(playername, "x_teleport");
	if (not player) then
		LogAdmins("[AEGIS] %q not found. Teleport Failed", playername);
		return;
	end
	player.exceptedTime = _time; --Catch for SSM Anticheat
	local spectating = player.actor:GetSpectatorMode() ~= 0;
	if (spectating) then
		LogAdmins("[AEGIS] %q is spectating. Teleport Failed", player:GetName());
		return;
	end
	local alive = player.actor:GetHealth()>0;
	if (not alive) then
		LogAdmins("[AEGIS] %q is not alive. Teleport Failed", player:GetName());
		return;
	end
	if (player:IsOnVehicle()) then
		local vehicle = System.GetEntity(player.actor:GetLinkedVehicleId());
		vehicle.vehicle:ExitVehicle(player.id);
		LogAdmins("[AEGIS] %q is in a vehicle. Removing from vehicle and re-issueing teleport command.", player:GetName());
		Script.SetTimer(500, function() XTelePort(playername, a, b, c); end);
		return;
	end
	local x = tonumber(a);
	local y = tonumber(b);
	local z = tonumber(c);
	local lca = string.lower(tostring(a));
	if (x and y and z) then
		LogAdmins("[AEGIS] TELEPORTING %s to %7.2f %7.2f %7.2f", player:GetName(), x, y, z);
		g_gameRules.game:SetInvulnerability(player.id, true, 2); -- avoid teleportation smash
		g_gameRules.game:MovePlayer(player.id, {x = x, y = y, z = z}, player:GetWorldAngles());
		if (XCfgVar.sendChatOnXTeleport) then
			XSendChatToAll(nil, "%s teleported to %7.2f %7.2f %7.2f (ADMIN DECISION)", player:GetName(), x, y, z);
		end
		return;
	end
	if (lca == "behind") then
		local target = XSimplePlayerForName(b, "x_teleport");
		if (not target) then
			return;
		end
		local tgtpos, tgtdir = XCalcSpawnPlayerPos(target, -1);
		LogAdmins("[AEGIS] TELEPORTING %s to %7.2f %7.2f %7.2f (behind %s)", player:GetName(), tgtpos.x, tgtpos.y, tgtpos.z, target:GetName());
		g_gameRules.game:SetInvulnerability(player.id, true, 2); -- avoid teleportation smash
		g_gameRules.game:MovePlayer(player.id, tgtpos, {x = 0, y = 0, z = math.atan2(tgtdir.y, tgtdir.x)-g_Pi2});
		if (XCfgVar.sendChatOnXTeleport) then
			XSendChatToAll(nil, "%s teleported behind %s (ADMIN DECISION)", player:GetName(), target:GetName());
		end
		return;
	end
	local pos, dir = XCalcSpawnPlayerPos(player, 0);
	if (lca == "up" and y) then
		pos.z = pos.z + y;
		LogAdmins("[AEGIS] TELEPORTING %s to %7.2f %7.2f %7.2f", player:GetName(), pos.x, pos.y, pos.z);
		g_gameRules.game:SetInvulnerability(player.id, true, 2); -- avoid teleportation smash
		g_gameRules.game:MovePlayer(player.id, pos, player:GetWorldAngles());
		if (XCfgVar.sendChatOnXTeleport) then
			XSendChatToAll(nil, "%s teleported up %d meters (ADMIN DECISION)", player:GetName(), y);
		end
		return;
	end
	if (lca == "fwd" and y) then
		ScaleVectorInPlace(dir, y);
		FastSumVectors(pos, pos, dir);
		LogAdmins("[AEGIS] TELEPORTING %s to %7.2f %7.2f %7.2f", player:GetName(), pos.x, pos.y, pos.z);
		g_gameRules.game:SetInvulnerability(player.id, true, 2); -- avoid teleportation smash
		g_gameRules.game:MovePlayer(player.id, pos, player:GetWorldAngles());
		if (XCfgVar.sendChatOnXTeleport) then
			XSendChatToAll(nil, "%s teleported forward %d meters (ADMIN DECISION)", player:GetName(), y);
		end
		return;
	end
	LogAdmins("[AEGIS] player pos: %7.2f %7.2f %7.2f %s", pos.x, pos.y, pos.z, player:GetName());
end

-- =============================================================================
-- Function: XTime
--
-- This function changes the remaining time to the input value.
--
-- \param[in] time
-- The time to set the remaining game time to in minutes.
-- =============================================================================
function XTime(time)
	local originalTimeLimit = System.GetCVar("g_timelimit");
	System.SetCVar("g_timelimit", time);
	g_gameRules.game:ResetGameTime();
	System.SetCVar("g_timelimit", originalTimeLimit);

	LogAdmins("$3[AEGIS] Set remaining time to "..time.." minutes.");
end

-- =============================================================================
-- Function: XKill
--
-- This function kills the specified player.
--
-- \param[in] playername
-- The name of the player to be killed.
--
-- \param[in] ...
-- Unknown number of parameters that make up a message stating why the player
-- was killed.
-- =============================================================================
function XKill(playername)
	-- We need to get the player object of the player to be killed. The player
	-- name can be used to look up the object for that player.
	local player = XSimplePlayerForName(playername, "x_kill");

	-- If the player object is not valid then exit the function without doing
	-- anything. This will prevent the server from crashing if an error with the
	-- player object occurs.
	if (not player) then
		return;
	end

	-- Before we kill the player we need to make sure that the player is alive
	-- and not in spectator mode.
	if (player.actor:GetHealth()>0 and player.actor:GetSpectatorMode() == 0) then
		g_gameRules:KillPlayer(player);

		-- If the server has configured the send chat on kill feature then we
		-- need to send a message to all players chat window stating that the
		-- player was killed due to an admin decision.
		if (XCfgVar.sendChatOnXKill) then
			XSendChatToAll(nil, "%s was killed by admin. (ADMIN DECISION)", player:GetName());
		end
	end
end

-- ===============================================

function XVersion()
	LogAdmins("$3[AEGIS] Version: $6%s", AEGIS.VERSION);
end

-- ===============================================

function XPrepareMap()
	SetSSMChatEntity();
end

function SetSSMChatEntity()
	if (System.GetEntityByName(XCfgVar.ChatEntityName)) then
		LogAlways("$6[AEGIS] Chat Entity Already Set");
		return;
	end
	local params = {
		class = "SMG";
		position = {x = 1, y = 1, z = 3000};
		orientation = {x = 0, y = 0, z = 1};
		name = XCfgVar.ChatEntityName;
		properties = {
			bAdjustToTerrain = 1;
			Respawn = {
				bRespawn = 1;
				nTimer = 1;
				bUnique = 1;
				};
				};
				};
	local SSMChat = System.SpawnEntity(params);
	if (SSMChat) then
        CreateActor(SSMChat);
		if (g_gameRules and g_gameRules.class == "InstantAction") then
			g_gameRules.game:SetTeam(0, SSMChat.id);
		else
			g_gameRules.game:SetTeam(2, SSMChat.id);
		end
		LogAlways("$3[AEGIS] Succesfully spawned chat entity %s", SSMChat:GetName());
	else
		LogAdmins("$4[AEGIS] Spawn of %s chat entity failed", class);
	end
	return
end
-- ===============================================

function XExportSpawnInfo(spawned, params)
	params.position = spawned:GetWorldPos();
	params.orientation = spawned:GetDirectionVector(1);
	local fname = XGetSSMFileForName(spawned:GetName()..".lua");
	local file, err = io.open(fname, "w+");
	if (not file) then
		LogAlways("[AEGIS] FILE ERROR: %s", err);
		XTableDumper:Reset();
		XTableDumper:Dump("params", params);
	else
		XTableDumper:Reset(file);
		file:write("-- EXPORTED SPAWN INFO\n");
		file:write("\n");
		XTableDumper:Dump("params", params);
		file:close();
	end
end

AEGIS.chatCommands = {};

XMutedPlayers = {};
function XMute(playername)
    local player = XSimplePlayerForName(playername, "x_mute");

    if(not player) then
	return;
    end
    player.XIsMuted = true;
end
function XUnmute(playername)
    local player = XSimplePlayerForName(playername, "x_unmute");

    if(not player) then
	return;
    end

    player.XIsMuted = false;
end


function XIsNoob(playername)
    local player = XSimplePlayerForName(playername, "x_isnoob");
    if(not player) then
	return;
    end
    local kills = (self.game:GetSynchedEntityValue(player.id, self.SCORE_KILLS_KEY) or 0);
    local deaths = (self.game:GetSynchedEntityValue(player.id, self.SCORE_DEATHS_KEY) or 0);
    local KDR;
    local name = player:GetName();
    local vehicleId=player.actor:GetLinkedVehicleId();
    if(deaths>0) then
	KDR = kills/deaths;
    end
    if KDR and (KDR<.2) then
	return true;
    elseif (vehicleId) then
	local vehicle=System.GetEntity(vehicleId);
	if(vehicle.class=="US_vtol") then
	    return true;
	end
    elseif(XCfgVars.funnyNames[name]) then
	return true;
    else
	return false;
    end
end

function XOnDllLoad(ver)
    LogAlways("AEGIS.dll ("..ver..") loaded successfully");
    XDllLoaded = true;
end




-- ===============================================
-- PREMIUM COMMANDS
-- ===============================================

function XPremiumList()
	for i,n in pairs(XCfgVar.premiumIds) do
		local player = XGetPlayerForIdentityValue("profile", i);
		if (player) then
			player = XFormat("$3%s",player:GetName());
		else
			player = "$4Offline";
		end
		LogAdmins("[AEGIS] Profile: %s Player: %s",i,player);
	end
	LogAdmins("$3[AEGIS] -- End of the list of recognized premium players.");
end


function XPremiumIdAdd(id)
	if (XCfgVar.premiumIds[id]) then
		LogAdmins("$6[AEGIS] id %s is already in the list of recognized premium players.",id);
		return;
	end
	XCfgVar.premiumIds[id]=true;
	LogAdmins("$3[AEGIS] Added id %s to the list of recognized premium players.",id);
end


function XPremiumIdRemove(id)
	if (XCfgVar.premiumIds[id]) then
		XCfgVar.premiumIds[id] = nil;
		LogAdmins("$3[AEGIS] Deleted id %s from the list of recognized premium players.",id);
		return;
	else
	LogAdmins("$4[AEGIS] id %s is not on the list of recognized premium players.",id);
	end
end


function XPremiumAdd(name)
	local player = XSimplePlayerForName(name,"x_premium_add");
	if (not player) then
		return;
	end
	local profile = XGetIdentityValue(player, "profile");
	if (profile=="0") then
		LogAdmins("$4[AEGIS] profile id of %q is unknown.",player:GetName());
		return;
	end
	if (XCfgVar.premiumIds[profile]) then
		LogAdmins("$6[AEGIS] %q is already in the list of recognized premium players.",player:GetName());
		return;
	end
	XCfgVar.premiumIds[profile] = true;
	LogAdmins("$3[AEGIS] Added %q with id %s to the list of recognized premium players.",player:GetName(),profile);
end


function XPremiumRemove(name)
	local player = XSimplePlayerForName(name,"x_premium_del");
	if (not player) then
		return;
	end
	local profile = XGetIdentityValue(player, "profile");
	if (profile=="0") then
		LogAdmins("$4[AEGIS] profile id of %q is unknown.",player:GetName());
		return
	end
	if (XCfgVar.premiumIds[profile]) then
		XCfgVar.premiumIds[profile] = nil;
		LogAdmins("$3[AEGIS] Deleted %q with id %s from the list of recognized premium players.",player:GetName(),profile);
		return;
	end
	LogAdmins("$6[AEGIS] %q is not on the list of recognized premium players.",player:GetName());
end







-- ===============================================

-- PLACE NOTHING AFTER THIS LINE!!!
AEGIS.Main = true;
